From 3c54e94c4394046e45cca656ec09cf9ac88f3208 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Sun, 16 Apr 2023 17:18:02 -0400 Subject: [PATCH 01/10] chore (core) move pattern related classes to their own folder --- packages/core/src/design.mjs | 2 +- packages/core/src/index.mjs | 4 ++-- .../src/{pattern.mjs => pattern/index.mjs} | 24 +++++++++---------- .../core/src/{ => pattern}/pattern-config.mjs | 2 +- .../src/{ => pattern}/pattern-draft-queue.mjs | 0 5 files changed, 16 insertions(+), 16 deletions(-) rename packages/core/src/{pattern.mjs => pattern/index.mjs} (98%) rename packages/core/src/{ => pattern}/pattern-config.mjs (99%) rename packages/core/src/{ => pattern}/pattern-draft-queue.mjs (100%) diff --git a/packages/core/src/design.mjs b/packages/core/src/design.mjs index 6d62ed70a1a..aa5b9caefbc 100644 --- a/packages/core/src/design.mjs +++ b/packages/core/src/design.mjs @@ -1,4 +1,4 @@ -import { Pattern } from './pattern.mjs' +import { Pattern } from './pattern/index.mjs' import { __loadDesignDefaults } from './config.mjs' ////////////////////////////////////////////// diff --git a/packages/core/src/index.mjs b/packages/core/src/index.mjs index 98224d8e5ea..38657c84c0d 100644 --- a/packages/core/src/index.mjs +++ b/packages/core/src/index.mjs @@ -1,13 +1,13 @@ import { Bezier } from 'bezier-js' import { Attributes } from './attributes.mjs' import { Design } from './design.mjs' -import { Pattern } from './pattern.mjs' +import { Pattern } from './pattern/index.mjs' import { Part } from './part.mjs' import { Point } from './point.mjs' import { Path } from './path.mjs' import { Snippet } from './snippet.mjs' import { Store } from './store.mjs' -import { hidePresets } from './pattern-config.mjs' +import { hidePresets } from './pattern/pattern-config.mjs' import { beamIntersectsCircle, beamIntersectsX, diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern/index.mjs similarity index 98% rename from packages/core/src/pattern.mjs rename to packages/core/src/pattern/index.mjs index d4d881d500d..69ba45fea5e 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern/index.mjs @@ -1,16 +1,16 @@ -import { Attributes } from './attributes.mjs' +import { Attributes } from '../attributes.mjs' import pack from 'bin-pack-with-constraints' -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 { __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, getPluginName } from './pattern-config.mjs' import { PatternDraftQueue } from './pattern-draft-queue.mjs' import cloneDeep from 'lodash.clonedeep' diff --git a/packages/core/src/pattern-config.mjs b/packages/core/src/pattern/pattern-config.mjs similarity index 99% rename from packages/core/src/pattern-config.mjs rename to packages/core/src/pattern/pattern-config.mjs index 7bac6c6fd2b..9fddf27759e 100644 --- a/packages/core/src/pattern-config.mjs +++ b/packages/core/src/pattern/pattern-config.mjs @@ -1,4 +1,4 @@ -import { __addNonEnumProp } from './utils.mjs' +import { __addNonEnumProp } from '../utils.mjs' export const hidePresets = { HIDE_ALL: { diff --git a/packages/core/src/pattern-draft-queue.mjs b/packages/core/src/pattern/pattern-draft-queue.mjs similarity index 100% rename from packages/core/src/pattern-draft-queue.mjs rename to packages/core/src/pattern/pattern-draft-queue.mjs From e59341effa2f7343f6d728ef4bbe8087d2c5cc26 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Sun, 16 Apr 2023 18:24:56 -0400 Subject: [PATCH 02/10] refactor (core) separate sampling into its own handler --- packages/core/src/pattern/index.mjs | 199 +-------------- .../src/pattern/pattern-draft-handler.mjs | 0 packages/core/src/pattern/pattern-sampler.mjs | 226 ++++++++++++++++++ 3 files changed, 231 insertions(+), 194 deletions(-) create mode 100644 packages/core/src/pattern/pattern-draft-handler.mjs create mode 100644 packages/core/src/pattern/pattern-sampler.mjs diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs index 69ba45fea5e..ef9ebcea89d 100644 --- a/packages/core/src/pattern/index.mjs +++ b/packages/core/src/pattern/index.mjs @@ -13,6 +13,7 @@ import { version } from '../../data.mjs' import { __loadPatternDefaults } from '../config.mjs' import { PatternConfig, getPluginName } from './pattern-config.mjs' import { PatternDraftQueue } from './pattern-draft-queue.mjs' +import { PatternSampler } from './pattern-sampler.mjs' import cloneDeep from 'lodash.clonedeep' ////////////////////////////////////////////// @@ -254,14 +255,7 @@ Pattern.prototype.getRenderProps = function () { * @return {object} this - The Pattern instance */ Pattern.prototype.sample = function () { - this.__init() - 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[0].sample.type === 'models') { - return this.sampleModels(this.settings[0].sample.models, this.settings[0].sample.focus || false) - } + return new PatternSampler(this).sample() } /** @@ -270,13 +264,7 @@ Pattern.prototype.sample = function () { * @return {object} this - The Pattern instance */ 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() + return new PatternSampler(this).sampleMeasurement(measurementName) } /** @@ -285,13 +273,7 @@ Pattern.prototype.sampleMeasurement = function (measurementName) { * @return {object} this - The Pattern instance */ 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() + return new PatternSampler(this).sampleModels(models, focus) } /** @@ -300,13 +282,7 @@ Pattern.prototype.sampleModels = function (models, focus = false) { * @return {object} this - The Pattern instance */ Pattern.prototype.sampleOption = function (optionName) { - this.store.log.debug(`Sampling option \`${optionName}\``) - this.__runHooks('preSample') - this.__applySettings(this.__optionSets(optionName)) - this.__init() - this.__runHooks('postSample') - - return this.draft() + return new PatternSampler(this).sampleOption(optionName) } /** @@ -527,36 +503,6 @@ Pattern.prototype.__isStackHidden = function (stackName) { return true } -/** - * Generates an array of settings.options objects for sampling a list or boolean option - * - * @private - * @param {string} optionName - Name of the option to sample - * @return {Array} sets - The list of settings objects - */ -Pattern.prototype.__listBoolOptionSets = function (optionName) { - let option = this.config.options[optionName] - const base = this.__setBase() - const sets = [] - let run = 1 - if (typeof option.bool !== 'undefined') option = { list: [false, true] } - 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 -} - /** * Generates an array of settings.absoluteOptions objects for sampling a list option * @@ -729,75 +675,6 @@ Pattern.prototype.__macro = function (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 - */ -Pattern.prototype.__measurementSets = function (measurementName) { - let val = this.settings[0].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 - 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 -} - -/** - * 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 - */ -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 -} - /** * Determines whether a part is needed, depending on the 'only' setting and the configured dependencies * @@ -830,58 +707,6 @@ Pattern.prototype.__needs = function (partName, set = 0) { return false } -/** - * 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 = [] - 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) - let factor = 1 - let step, val - let numberRuns = 10 - let stepFactor = numberRuns - 1 - 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 - 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 - const base = this.__setBase() - for (let run = 1; run <= numberRuns; run++) { - const settings = { - ...base, - options: { - ...base.options, - }, - idPrefix: `sample-${run}`, - partClasses: `sample-${run}`, - } - settings.options[optionName] = val - sets.push(settings) - val += step - if (typeof option.count !== 'undefined' || typeof option.mm !== 'undefined') - val = Math.round(val) - } - - return sets -} - /** * Packs stacks in a 2D space and sets pattern size * @@ -999,20 +824,6 @@ Pattern.prototype.__runHooks = function (hookName, data = false) { } } -/** - * Returns the base/defaults to generate a set of settings - * - * @private - * @return {object} settings - The settings object - */ -Pattern.prototype.__setBase = function () { - return { - ...this.settings[0], - measurements: { ...(this.settings[0].measurements || {}) }, - options: { ...(this.settings[0].options || {}) }, - } -} - /** * Returns the absolute value of a snapped percentage option * diff --git a/packages/core/src/pattern/pattern-draft-handler.mjs b/packages/core/src/pattern/pattern-draft-handler.mjs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/pattern/pattern-sampler.mjs b/packages/core/src/pattern/pattern-sampler.mjs new file mode 100644 index 00000000000..5ccc6baec43 --- /dev/null +++ b/packages/core/src/pattern/pattern-sampler.mjs @@ -0,0 +1,226 @@ +export function PatternSampler(pattern) { + this.pattern = pattern +} + +PatternSampler.prototype.sample = function () { + this.pattern.__init() + const sampleSetting = this.pattern.settings[0].sample + if (sampleSetting.type === 'option') { + return this.sampleOption(sampleSetting.option) + } else if (sampleSetting.type === 'measurement') { + return this.sampleMeasurement(sampleSetting.measurement) + } else if (sampleSetting.type === 'models') { + return this.sampleModels(sampleSetting.models, sampleSetting.focus || false) + } + return this.pattern +} + +/** + * Handles measurement sampling + * + * @return {object} this - The Pattern instance + */ +PatternSampler.prototype.sampleMeasurement = function (measurementName) { + this.pattern.store.log.debug(`Sampling measurement \`${measurementName}\``) + this.pattern.__runHooks('preSample') + this.pattern.__applySettings(this.__measurementSets(measurementName)) + this.pattern.__init() + this.pattern.__runHooks('postSample') + + return this.pattern.draft() +} + +/** + * Handles models sampling + * + * @return {object} this - The Pattern instance + */ +PatternSampler.prototype.sampleModels = function (models, focus = false) { + this.pattern.store.log.debug(`Sampling models \`${Object.keys(models).join(', ')}\``) + this.pattern.__runHooks('preSample') + this.pattern.__applySettings(this.__modelSets(models, focus)) + this.pattern.__init() + this.pattern.__runHooks('postSample') + + return this.pattern.draft() +} + +/** + * Handles option sampling + * + * @return {object} this - The Pattern instance + */ +PatternSampler.prototype.sampleOption = function (optionName) { + this.pattern.store.log.debug(`Sampling option \`${optionName}\``) + this.pattern.__runHooks('preSample') + this.pattern.__applySettings(this.__optionSets(optionName)) + this.pattern.__init() + this.pattern.__runHooks('postSample') + + return this.pattern.draft() +} + +/** + * Returns the base/defaults to generate a set of settings + * + * @private + * @return {object} settings - The settings object + */ +PatternSampler.prototype.__setBase = function () { + return { + measurements: {}, + options: {}, + ...this.pattern.settings[0], + } +} + +/** + * Generates an array of settings.options objects for sampling a list or boolean option + * + * @private + * @param {string} optionName - Name of the option to sample + * @return {Array} sets - The list of settings objects + */ +PatternSampler.prototype.__listBoolOptionSets = function (optionName) { + let option = this.pattern.config.options[optionName] + const base = this.__setBase() + const sets = [] + let run = 1 + if (typeof option.bool !== 'undefined') option = { list: [false, true] } + 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 +} + +/** + * 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 + */ +PatternSampler.prototype.__measurementSets = function (measurementName) { + let val = this.pattern.settings[0].measurements[measurementName] + if (val === undefined) + this.pattern.store.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 +} + +/** + * 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 + */ +PatternSampler.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 +} + +/** + * 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 + */ +PatternSampler.prototype.__optionSets = function (optionName) { + const sets = [] + if (!(optionName in this.pattern.config.options)) return sets + let option = this.pattern.config.options[optionName] + if (typeof option.list === 'object' || typeof option.bool !== 'undefined') + return this.__listBoolOptionSets(optionName) + let factor = 1 + let step, val + let numberRuns = 10 + let stepFactor = numberRuns - 1 + 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 + 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 + const base = this.__setBase() + for (let run = 1; run <= numberRuns; run++) { + const settings = { + ...base, + options: { + ...base.options, + }, + idPrefix: `sample-${run}`, + partClasses: `sample-${run}`, + } + settings.options[optionName] = val + sets.push(settings) + val += step + if (typeof option.count !== 'undefined' || typeof option.mm !== 'undefined') + val = Math.round(val) + } + + return sets +} From c5baaab234e6d0fd1c7404d9dd902f8c32524cc6 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Sun, 16 Apr 2023 23:59:21 -0400 Subject: [PATCH 03/10] refactor (core) move plugin and hook handling to its own class --- packages/core/src/pattern/index.mjs | 164 ++-------------- packages/core/src/pattern/pattern-config.mjs | 12 +- packages/core/src/pattern/pattern-plugins.mjs | 185 ++++++++++++++++++ packages/core/tests/pattern-init.test.mjs | 44 ++--- packages/core/tests/pattern-other.test.mjs | 4 +- 5 files changed, 225 insertions(+), 184 deletions(-) create mode 100644 packages/core/src/pattern/pattern-plugins.mjs diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs index ef9ebcea89d..6903264c443 100644 --- a/packages/core/src/pattern/index.mjs +++ b/packages/core/src/pattern/index.mjs @@ -11,9 +11,10 @@ import { Store } from '../store.mjs' import { Hooks } from '../hooks.mjs' import { version } from '../../data.mjs' import { __loadPatternDefaults } from '../config.mjs' -import { PatternConfig, getPluginName } from './pattern-config.mjs' +import { PatternConfig } from './pattern-config.mjs' import { PatternDraftQueue } from './pattern-draft-queue.mjs' import { PatternSampler } from './pattern-sampler.mjs' +import { PatternPlugins, getPluginName } from './pattern-plugins.mjs' import cloneDeep from 'lodash.clonedeep' ////////////////////////////////////////////// @@ -35,7 +36,6 @@ export function Pattern(designConfig = {}) { this.setStores = [] // Per-set stores // Non-enumerable properties - __addNonEnumProp(this, 'plugins', {}) __addNonEnumProp(this, 'width', 0) __addNonEnumProp(this, 'height', 0) __addNonEnumProp(this, 'autoLayout', { stacks: {} }) @@ -49,7 +49,8 @@ export function Pattern(designConfig = {}) { __addNonEnumProp(this, '__initialized', false) __addNonEnumProp(this, 'config.parts', {}) __addNonEnumProp(this, 'config.resolvedDependencies', {}) - __addNonEnumProp(this, '__storeMethods', new Set()) + + __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 @@ -202,7 +203,7 @@ Pattern.prototype.getRenderProps = function () { this.store.log.info('Gathering render props') // Run pre-render hook let svg = new Svg(this) - svg.hooks = this.hooks + svg.hooks = this.plugins.hooks this.__pack() svg.__runHooks('preRender') @@ -294,11 +295,7 @@ Pattern.prototype.sampleOption = function (optionName) { * @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 }) + this.plugins.on(hook, method, data) return this } @@ -310,7 +307,7 @@ Pattern.prototype.on = function (hook, method, data) { */ Pattern.prototype.render = function () { this.svg = new Svg(this) - this.svg.hooks = this.hooks + this.svg.hooks = this.plugins.hooks return this.__pack().svg.render() } @@ -323,13 +320,7 @@ Pattern.prototype.render = function () { * @return {object} this - The Pattern instance */ Pattern.prototype.use = function (plugin, data) { - const name = getPluginName(plugin) - if (!this.plugins?.[name]) - return plugin.plugin && plugin.condition - ? this.__useIf(plugin, data) // Conditional plugin - : this.__loadPlugin(plugin, data) // Regular plugin - - this.store.log.info(`Plugin \`${name}\` was requested, but it's already loaded. Skipping.`) + this.plugins.use(plugin, data, this.settings) return this } @@ -347,7 +338,7 @@ Pattern.prototype.use = function (plugin, data) { Pattern.prototype.__createSetStore = function () { const store = new Store() store.set('data', this.store.data) - store.extend([...this.__storeMethods]) + store.extend([...this.plugins.__storeMethods]) return store } @@ -400,15 +391,15 @@ Pattern.prototype.__createPartWithContext = function (name, set) { config: this.config, settings: this.settings[set], store: this.setStores[set], - macros: this.macros, + macros: this.plugins.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] + for (const macro in this.plugins.macros) { + part[__macroName(macro)] = this.plugins.macros[macro] } return part @@ -457,9 +448,10 @@ Pattern.prototype.__init = function () { */ this.__resolveParts() // Resolves parts .__resolveConfig() // Gets the config from the resolver - .__loadOptionDefaults() // Merges default options with user provided ones - .__loadPlugins() // Loads plugins .__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') @@ -577,104 +569,6 @@ Pattern.prototype.__loadOptionDefaults = function () { return this } -/** - * Loads a plugin - * - * @private - * @param {object} plugin - The plugin object, or an object with `plugin` and `condition` keys - * @param {object} data - Any plugin data to load - * @return {Pattern} this - The Pattern instance - */ -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.store.log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``) - - return this -} - -/** - * 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 -} - -/** - * 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 () { - if (!this.config.plugins) return this - for (const plugin in this.config.plugins) - this.use(this.config.plugins[plugin], this.config.plugins[plugin]?.data) - - 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)) { - for (const method of plugin.store) this.__storeMethods.add(method) - } else this.store.log.warning(`Plugin store methods should be an Array`) - - return this -} - -/** - * 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 -} - /** * Determines whether a part is needed, depending on the 'only' setting and the configured dependencies * @@ -815,7 +709,7 @@ Pattern.prototype.__resolveParts = function () { */ Pattern.prototype.__runHooks = function (hookName, data = false) { if (data === false) data = this - let hooks = this.hooks[hookName] + let hooks = this.plugins.hooks[hookName] if (hooks.length > 0) { this.store.log.debug(`Running \`${hookName}\` hooks`) for (let hook of hooks) { @@ -863,32 +757,6 @@ Pattern.prototype.__snappedPercentageOption = function (optionName, set) { return abs } -/** - * Loads a conditional plugin - * - * @private - * @param {object} plugin - An object with `plugin` and `condition` keys - * @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++ - } - if (load > 0) { - this.store.log.info( - `Condition met: Loaded plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` - ) - this.__loadPlugin(plugin.plugin, plugin.data) - } else { - this.store.log.info( - `Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` - ) - } - - return this -} - /** * Determines whether a part is wanted, depending on the 'only' setting and the configured dependencies * diff --git a/packages/core/src/pattern/pattern-config.mjs b/packages/core/src/pattern/pattern-config.mjs index 9fddf27759e..69659d62fd0 100644 --- a/packages/core/src/pattern/pattern-config.mjs +++ b/packages/core/src/pattern/pattern-config.mjs @@ -1,4 +1,5 @@ import { __addNonEnumProp } from '../utils.mjs' +import { getPluginName } from './pattern-plugins.mjs' export const hidePresets = { HIDE_ALL: { @@ -13,17 +14,6 @@ export const hidePresets = { }, } -/** - * Get the name of the given plugin config - * - * @param {(Object|Object[])} plugin the plugin to get the name of - * @return {(string|false)} the name, or false if there isn't one - */ -export function getPluginName(plugin) { - const toCheck = Array.isArray(plugin) ? plugin[0] : plugin - return toCheck.name || toCheck.plugin?.name || false -} - ///////////////// // CONSTRUCTOR // ///////////////// diff --git a/packages/core/src/pattern/pattern-plugins.mjs b/packages/core/src/pattern/pattern-plugins.mjs new file mode 100644 index 00000000000..e41d6a5b78d --- /dev/null +++ b/packages/core/src/pattern/pattern-plugins.mjs @@ -0,0 +1,185 @@ +import { Hooks } from '../hooks.mjs' + +/** + * Get the name of the given plugin config + * + * @param {(Object|Object[])} plugin the plugin to get the name of + * @return {(string|false)} the name, or false if there isn't one + */ +export function getPluginName(plugin) { + const toCheck = Array.isArray(plugin) ? plugin[0] : plugin + return toCheck.name || toCheck.plugin?.name || false +} + +export function PatternPlugins(pattern) { + this.store = pattern.store + + this.plugins = {} + this.hooks = new Hooks() + this.macros = {} + this.__storeMethods = new Set() +} + +/** + * 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 + */ +PatternPlugins.prototype.use = function (plugin, data, settings = [{}]) { + const name = getPluginName(plugin) + if (!this.plugins?.[name]) + return plugin.plugin && plugin.condition + ? this.__useIf(plugin, data, settings) // Conditional plugin + : this.__loadPlugin(plugin, data) // Regular plugin + + this.store.log.info(`Plugin \`${name}\` was requested, but it's already loaded. Skipping.`) + + return this +} + +/** + * 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 + */ +PatternPlugins.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 +} + +/** + * Loads the plugins that are part of the config + * + * @private + * @return {Pattern} this - The Pattern instance + */ +PatternPlugins.prototype.loadConfigPlugins = function (config, settings) { + if (!config.plugins) return this + for (const plugin in config.plugins) + this.use(config.plugins[plugin], config.plugins[plugin]?.data, settings) + return this +} + +/** + * Loads a plugin + * + * @private + * @param {object} plugin - The plugin object, or an object with `plugin` and `condition` keys + * @param {object} data - Any plugin data to load + * @return {Pattern} this - The Pattern instance + */ +PatternPlugins.prototype.__loadPlugin = function (plugin, data) { + const name = getPluginName(plugin) + this.plugins[name] = plugin + if (plugin.hooks) this.__loadPluginHooks(plugin, data) + if (plugin.macros) this.__loadPluginMacros(plugin) + if (plugin.store) this.__loadPluginStoreMethods(plugin) + this.store.log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``) + + return this +} + +/** + * 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 + */ +PatternPlugins.prototype.__loadPluginHooks = function (plugin, data) { + // console.log('hooks', plugin) + 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 +} + +/** + * Loads a plugin's macros + * + * @private + * @param {object} plugin - The plugin object + * @return {Pattern} this - The Pattern instance + */ +PatternPlugins.prototype.__loadPluginMacros = function (plugin) { + // console.log('macros', plugin) + for (let macro in plugin.macros) { + if (typeof plugin.macros[macro] === 'function') { + this.__macro(macro, plugin.macros[macro]) + } + } +} + +/** + * Loads a plugin's store methods + * + * @private + * @param {object} plugin - The plugin object + * @return {Pattern} this - The Pattern instance + */ +PatternPlugins.prototype.__loadPluginStoreMethods = function (plugin) { + if (Array.isArray(plugin.store)) { + for (const method of plugin.store) this.__storeMethods.add(method) + } else this.store.log.warning(`Plugin store methods should be an Array`) + + // console.log('store', plugin, this.__storeMethods) + return this +} + +/** + * 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 + */ +PatternPlugins.prototype.__macro = function (key, method) { + this.macros[key] = method + + return this +} + +/** + * Loads a conditional plugin + * + * @private + * @param {object} plugin - An object with `plugin` and `condition` keys + * @return {Pattern} this - The Pattern instance + */ +PatternPlugins.prototype.__useIf = function (plugin, settings = [{}]) { + let load = 0 + for (const set of settings) { + if (plugin.condition(set)) load++ + } + if (load > 0) { + this.store.log.info( + `Condition met: Loaded plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` + ) + this.__loadPlugin(plugin.plugin, plugin.data) + } else { + this.store.log.info( + `Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` + ) + } + + return this +} diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index 743d8bc5ab7..718fa363988 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -18,7 +18,7 @@ describe('Pattern', () => { expect(Array.isArray(pattern.setStores)).to.equal(true) expect(typeof pattern.store).to.equal('object') expect(typeof pattern.config).to.equal('object') - expect(Object.keys(pattern).length).to.equal(5) + expect(Object.keys(pattern)).to.have.lengthOf(5) }) it('Pattern constructor should add non-enumerable properties', () => { @@ -26,12 +26,10 @@ describe('Pattern', () => { const pattern = new Pattern() expect(typeof pattern.plugins).to.equal('object') expect(typeof pattern.autoLayout).to.equal('object') - expect(typeof pattern.hooks).to.equal('object') expect(typeof pattern.Point).to.equal('function') expect(typeof pattern.Path).to.equal('function') expect(typeof pattern.Snippet).to.equal('function') expect(typeof pattern.Attributes).to.equal('function') - expect(typeof pattern.macros).to.equal('object') // expect(typeof pattern.__designParts).to.equal('object') // expect(typeof pattern.config.inject).to.equal('object') // expect(typeof pattern.config.directDependencies).to.equal('object') @@ -58,7 +56,7 @@ describe('Pattern', () => { absoluteOptions: {}, } for (const [key, value] of Object.entries(dflts)) { - if (typeof value === 'object') expect(Object.keys(value).length).to.equal(0) + if (typeof value === 'object') expect(Object.keys(value)).to.have.lengthOf(0) else expect(pattern.settings[0][key]).to.equal(value) } }) @@ -120,19 +118,19 @@ describe('Pattern', () => { }) it('Pattern.__init() should resolve required measurements', () => { - expect(pattern.config.measurements.length).to.equal(2) + expect(pattern.config.measurements).to.have.lengthOf(2) expect(pattern.config.measurements[0]).to.equal('head') expect(pattern.config.measurements[1]).to.equal('knee') }) it('Pattern.__init() should resolve optional measurements', () => { - expect(pattern.config.optionalMeasurements.length).to.equal(2) + expect(pattern.config.optionalMeasurements).to.have.lengthOf(2) expect(pattern.config.optionalMeasurements[0]).to.equal('chest') expect(pattern.config.optionalMeasurements[1]).to.equal('waist') }) it('Pattern.__init() should resolve options', () => { - expect(Object.keys(pattern.config.options).length).to.equal(3) + expect(Object.keys(pattern.config.options)).to.have.lengthOf(3) for (const [key, value] of Object.entries(partA.options.optA)) { expect(pattern.config.options.optA[key]).to.equal(value) } @@ -149,7 +147,7 @@ describe('Pattern', () => { }) it('Pattern.__init() should resolve plugins', () => { - expect(Object.keys(pattern.config.plugins).length).to.equal(1) + expect(Object.keys(pattern.config.plugins)).to.have.lengthOf(1) }) it('Pattern.__init() should set config data in the store', () => { @@ -160,12 +158,12 @@ describe('Pattern', () => { it('Pattern.__init() should resolve dependencies', () => { expect(typeof pattern.config.resolvedDependencies).to.equal('object') expect(Array.isArray(pattern.config.resolvedDependencies['test.partA'])).to.equal(true) - expect(pattern.config.resolvedDependencies['test.partA'].length).to.equal(0) + expect(pattern.config.resolvedDependencies['test.partA']).to.have.lengthOf(0) expect(Array.isArray(pattern.config.resolvedDependencies['test.partB'])).to.equal(true) - expect(pattern.config.resolvedDependencies['test.partB'].length).to.equal(1) + expect(pattern.config.resolvedDependencies['test.partB']).to.have.lengthOf(1) expect(pattern.config.resolvedDependencies['test.partB'][0]).to.equal('test.partA') expect(Array.isArray(pattern.config.resolvedDependencies['test.partC'])).to.equal(true) - expect(pattern.config.resolvedDependencies['test.partC'].length).to.equal(2) + expect(pattern.config.resolvedDependencies['test.partC']).to.have.lengthOf(2) expect( pattern.config.resolvedDependencies['test.partC'].indexOf('test.partA') !== -1 ).to.equal(true) @@ -325,13 +323,13 @@ describe('Pattern', () => { const design = new Design({ parts: [partC] }) const pattern = new design().addPart(partR).draft() // Measurements - expect(pattern.config.measurements.length).to.equal(4) + expect(pattern.config.measurements).to.have.lengthOf(4) expect(pattern.config.measurements.indexOf('measieA') === -1).to.equal(false) expect(pattern.config.measurements.indexOf('measieB') === -1).to.equal(false) expect(pattern.config.measurements.indexOf('measieC') === -1).to.equal(false) expect(pattern.config.measurements.indexOf('measieR') === -1).to.equal(false) // Optional measurements - expect(pattern.config.optionalMeasurements.length).to.equal(4) + expect(pattern.config.optionalMeasurements).to.have.lengthOf(4) expect(pattern.config.optionalMeasurements.indexOf('optmeasieA') === -1).to.equal(false) expect(pattern.config.optionalMeasurements.indexOf('optmeasieB') === -1).to.equal(false) expect(pattern.config.optionalMeasurements.indexOf('optmeasieC') === -1).to.equal(false) @@ -473,13 +471,13 @@ describe('Pattern', () => { const design = new Design({ parts: [partD] }) const pattern = new design().draft() // Measurements - expect(pattern.config.measurements.length).to.equal(4) + expect(pattern.config.measurements).to.have.lengthOf(4) expect(pattern.config.measurements.indexOf('measieA') === -1).to.equal(false) expect(pattern.config.measurements.indexOf('measieB') === -1).to.equal(false) expect(pattern.config.measurements.indexOf('measieC') === -1).to.equal(false) expect(pattern.config.measurements.indexOf('measieD') === -1).to.equal(false) // Optional measurements - expect(pattern.config.optionalMeasurements.length).to.equal(4) + expect(pattern.config.optionalMeasurements).to.have.lengthOf(4) expect(pattern.config.optionalMeasurements.indexOf('optmeasieA') === -1).to.equal(false) expect(pattern.config.optionalMeasurements.indexOf('optmeasieB') === -1).to.equal(false) expect(pattern.config.optionalMeasurements.indexOf('optmeasieC') === -1).to.equal(false) @@ -578,7 +576,7 @@ describe('Pattern', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft() - expect(pattern.hooks.preRender.length).to.equal(1) + expect(pattern.plugins.hooks.preRender).to.have.lengthOf(1) }) it('Pattern.__init() should load array of plugins', () => { @@ -608,7 +606,7 @@ describe('Pattern', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.__init() - expect(pattern.hooks.preRender.length).to.equal(2) + expect(pattern.plugins.hooks.preRender).to.have.lengthOf(2) }) it('Pattern.__init() should load conditional plugin if condition is met', () => { @@ -629,8 +627,8 @@ describe('Pattern', () => { } const design = new Design({ parts: [part] }) const pattern = new design() - pattern.draft() - expect(pattern.hooks.preRender.length).to.equal(1) + pattern.__init() + expect(pattern.plugins.hooks.preRender).to.have.lengthOf(1) }) it('Pattern.__init() should not load conditional plugin if condition is not mett', () => { @@ -651,7 +649,7 @@ describe('Pattern', () => { } const design = new Design({ parts: [part] }) const pattern = new design() - expect(pattern.hooks.preRender.length).to.equal(0) + expect(pattern.plugins.hooks.preRender).to.have.lengthOf(0) }) it('Pattern.__init() should load multiple conditional plugins', () => { @@ -686,7 +684,7 @@ describe('Pattern', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft() - expect(pattern.hooks.preRender.length).to.equal(1) + expect(pattern.plugins.hooks.preRender).to.have.lengthOf(1) }) it('Pattern.__init() should load a conditional plugin multiple times with different conditions', () => { @@ -718,7 +716,7 @@ describe('Pattern', () => { expect(pattern.config.plugins).to.be.an('object').that.has.all.keys('example1', 'example1_') expect(pattern.config.plugins.example1.plugin).to.deep.equal(plugin1) expect(pattern.config.plugins.example1_.plugin).to.deep.equal(plugin1) - expect(pattern.hooks.preRender.length).to.equal(1) + expect(pattern.plugins.hooks.preRender).to.have.lengthOf(1) }) it('Load conditional plugins that are also passing data', () => { @@ -757,7 +755,7 @@ describe('Pattern', () => { }) const pattern = new design() pattern.__init() - expect(pattern.hooks.preRender.length).to.equal(2) + expect(pattern.plugins.hooks.preRender).to.have.lengthOf(2) }) it('Pattern.__init() should register a hook via on', () => { diff --git a/packages/core/tests/pattern-other.test.mjs b/packages/core/tests/pattern-other.test.mjs index ba69d58a17d..c15c7ce07fa 100644 --- a/packages/core/tests/pattern-other.test.mjs +++ b/packages/core/tests/pattern-other.test.mjs @@ -90,8 +90,8 @@ describe('Pattern', () => { pattern.use(plugin) pattern.use({ plugin }) pattern.use({ plugin }) - expect(Object.keys(pattern.plugins).length).to.equal(1) - expect(Object.keys(pattern.plugins)[0]).to.equal('test') + expect(Object.keys(pattern.plugins.plugins)).to.have.lengthOf(1) + expect(Object.keys(pattern.plugins.plugins)[0]).to.equal('test') }) it('Should log an error of added parts do not have a draft method', () => { From 9e8db66c949e1595e9e41e4663e4dc181fd7da21 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Mon, 17 Apr 2023 10:49:32 -0400 Subject: [PATCH 04/10] refactor (core) move packing and rendering to its own class --- packages/core/src/pattern/index.mjs | 164 ++------------- .../core/src/pattern/pattern-renderer.mjs | 187 ++++++++++++++++++ packages/core/tests/pattern-draft.test.mjs | 4 +- 3 files changed, 200 insertions(+), 155 deletions(-) create mode 100644 packages/core/src/pattern/pattern-renderer.mjs diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs index 6903264c443..9ff25284860 100644 --- a/packages/core/src/pattern/index.mjs +++ b/packages/core/src/pattern/index.mjs @@ -15,6 +15,7 @@ import { PatternConfig } from './pattern-config.mjs' import { PatternDraftQueue } from './pattern-draft-queue.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' ////////////////////////////////////////////// @@ -194,60 +195,22 @@ Pattern.prototype.getConfig = function () { return this.__init().config } +/** + * Renders the pattern to SVG + * + * @return {string} svg - The rendered SVG + */ +Pattern.prototype.render = function () { + return new PatternRenderer(this).render() +} + /** 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 () { - this.store.log.info('Gathering render props') - // Run pre-render hook - let svg = new Svg(this) - svg.hooks = this.plugins.hooks - - this.__pack() - svg.__runHooks('preRender') - - let props = { svg } - props.width = this.width - props.height = this.height - props.autoLayout = this.autoLayout - props.settings = this.settings - 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], - } - } 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` - ) - } - } - props.parts.push(setParts) - } - props.stacks = {} - for (let s in this.stacks) { - if (!this.__isStackHidden(s)) { - props.stacks[s] = this.stacks[s].asProps() - } 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, - })), - } - - svg.__runHooks('postRender') - return props + return new PatternRenderer(this).getRenderProps() } /** @@ -300,18 +263,6 @@ Pattern.prototype.on = function (hook, 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.plugins.hooks - - return this.__pack().svg.render() -} - /** * Loads a plugin * @@ -405,26 +356,6 @@ Pattern.prototype.__createPartWithContext = function (name, set) { 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, - setStores: this.setStores, - } - - return stack -} - /** * Initializes the pattern coniguration and settings * @@ -601,79 +532,6 @@ Pattern.prototype.__needs = function (partName, set = 0) { return false } -/** - * Packs stacks in a 2D space and sets pattern size - * - * @private - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__pack = function () { - this.__runHooks('preLayout') - for (const set in this.settings) { - if (this.setStores[set].logs.error.length > 0) { - this.setStores[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[set], 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) { - // some plugins will add a width constraint to the settings, but we can safely pass undefined if not - let size = pack(bins, { inPlace: true, maxWidth: this.settings[0].maxWidth }) - 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[this.activeStack || 0].layout.stacks[stackId] - this.stacks[stackId].generateTransform(transforms) - } - } - } - - this.__runHooks('postLayout') - return this -} - /** * Gets the configuration for the config resolver and sets it on the pattern * @private diff --git a/packages/core/src/pattern/pattern-renderer.mjs b/packages/core/src/pattern/pattern-renderer.mjs new file mode 100644 index 00000000000..6215e0c5543 --- /dev/null +++ b/packages/core/src/pattern/pattern-renderer.mjs @@ -0,0 +1,187 @@ +import { Svg } from '../svg.mjs' +import { Stack } from '../stack.mjs' +import pack from 'bin-pack-with-constraints' + +export function PatternRenderer(pattern) { + this.pattern = pattern + this.autoLayout = pattern.autoLayout +} + +/** + * Renders the pattern to SVG + * + * @return {string} svg - The rendered SVG + */ +PatternRenderer.prototype.render = function () { + this.__startRender() + this.pattern.svg = this.svg + return this.svg.render() +} + +/** Returns props required to render this pattern through + * an external renderer (eg. a React component) + * + * @return {object} this - The Pattern instance + */ +PatternRenderer.prototype.getRenderProps = function () { + this.pattern.store.log.info('Gathering render props') + // Run pre-render hook + this.__startRender() + this.svg.__runHooks('preRender') + + let props = { + svg: this.svg, + width: this.pattern.width, + height: this.pattern.height, + autoLayout: this.pattern.autoLayout, + settings: this.pattern.settings, + parts: [], + } + + for (const set of this.pattern.parts) { + const setParts = {} + for (let p in set) { + if (!set[p].hidden) { + setParts[p] = { + ...set[p].asProps(), + store: this.pattern.setStores[set[p].set], + } + } else if (this.pattern.setStores[set.set]) { + this.pattern.setStores[set.set].log.info( + `Part${p} is hidden in set ${set.set}. Not adding to render props` + ) + } + } + props.parts.push(setParts) + } + props.stacks = {} + for (let s in this.pattern.stacks) { + if (!this.pattern.__isStackHidden(s)) { + props.stacks[s] = this.pattern.stacks[s].asProps() + } else this.pattern.store.log.info(`Stack ${s} is hidden. Skipping in render props.`) + } + props.logs = { + pattern: this.pattern.store.logs, + sets: this.pattern.setStores.map((store) => ({ + debug: store.logs.debug, + info: store.logs.info, + error: store.logs.error, + warning: store.logs.warning, + })), + } + + this.svg.__runHooks('postRender') + return props +} + +PatternRenderer.prototype.__startRender = function () { + this.svg = new Svg(this.pattern) + this.svg.hooks = this.pattern.plugins.hooks + this.__pack() + + return this +} + +PatternRenderer.prototype.__stack = function () { + // First, create all stacks + this.stacks = {} + const settings = this.pattern.settings + for (const set in settings) { + for (const [name, part] of Object.entries(this.pattern.parts[set])) { + const stackName = + settings[set].stackPrefix + + (typeof part.stack === 'function' ? part.stack(settings[set], name) : part.stack) + if (typeof this.stacks[stackName] === 'undefined') + this.stacks[stackName] = this.__createStackWithContext(stackName, set) + this.stacks[stackName].addPart(part) + } + } + + this.pattern.stacks = this.stacks +} +/** + * Packs stacks in a 2D space and sets pattern size + * + * @private + * @return {Pattern} this - The Pattern instance + */ +PatternRenderer.prototype.__pack = function () { + this.pattern.__runHooks('preLayout') + const { settings, setStores, parts } = this.pattern + for (const set in settings) { + if (setStores[set].logs.error.length > 0) { + setStores[set].log.warning(`One or more errors occured. Not packing pattern parts`) + return this + } + } + + this.__stack() + + 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.pattern.__isStackHidden(key)) { + stack.home() + if (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 (settings[0].layout === true) { + // some plugins will add a width constraint to the settings, but we can safely pass undefined if not + let size = pack(bins, { inPlace: true, maxWidth: settings[0].maxWidth }) + 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 settings[0].layout === 'object') { + this.width = settings[0].layout.width + this.height = settings[0].layout.height + for (let stackId of Object.keys(settings[0].layout.stacks)) { + // Some parts are added by late-stage plugins + if (this.stacks[stackId]) { + let transforms = settings[this.activeStack || 0].layout.stacks[stackId] + this.stacks[stackId].generateTransform(transforms) + } + } + } + + this.pattern.width = this.width + this.pattern.height = this.height + this.pattern.autoLayout = this.autoLayout + this.pattern.__runHooks('postLayout') + return this +} + +/** + * 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 + */ +PatternRenderer.prototype.__createStackWithContext = function (name) { + // Context object to add to Stack closure + const stack = new Stack() + stack.name = name + stack.context = { + config: this.pattern.config, + settings: this.pattern.settings, + setStores: this.pattern.setStores, + } + + return stack +} diff --git a/packages/core/tests/pattern-draft.test.mjs b/packages/core/tests/pattern-draft.test.mjs index b1b06850344..fae87165224 100644 --- a/packages/core/tests/pattern-draft.test.mjs +++ b/packages/core/tests/pattern-draft.test.mjs @@ -175,7 +175,7 @@ describe('Pattern', () => { ) }) - describe('Pattern.__pack()', () => { + describe('PatternRenderer.__pack()', () => { it('should get a part stack name from a function that uses settings', () => { const expectedName = 'namedStack' const front = { @@ -201,7 +201,7 @@ describe('Pattern', () => { const pattern = new Test() pattern.draft() - pattern.__pack() + pattern.getRenderProps() const stackNames = Object.keys(pattern.stacks) expect(stackNames).to.include(expectedName) From 8fea9a4beb2b2644c654fd4328aeab2b8157327f Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Mon, 17 Apr 2023 19:45:00 -0400 Subject: [PATCH 05/10] refactor (core) move draft handling into its own class --- packages/core/src/pattern/index.mjs | 194 +-------------- .../src/pattern/pattern-draft-handler.mjs | 0 packages/core/src/pattern/pattern-drafter.mjs | 227 ++++++++++++++++++ packages/core/tests/pattern-draft.test.mjs | 12 - packages/core/tests/pattern-other.test.mjs | 4 +- 5 files changed, 232 insertions(+), 205 deletions(-) delete mode 100644 packages/core/src/pattern/pattern-draft-handler.mjs create mode 100644 packages/core/src/pattern/pattern-drafter.mjs diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs index 9ff25284860..4a37e775321 100644 --- a/packages/core/src/pattern/index.mjs +++ b/packages/core/src/pattern/index.mjs @@ -1,5 +1,4 @@ import { Attributes } from '../attributes.mjs' -import pack from 'bin-pack-with-constraints' import { __addNonEnumProp, __macroName } from '../utils.mjs' import { Part } from '../part.mjs' import { Stack } from '../stack.mjs' @@ -12,7 +11,7 @@ import { Hooks } from '../hooks.mjs' import { version } from '../../data.mjs' import { __loadPatternDefaults } from '../config.mjs' import { PatternConfig } from './pattern-config.mjs' -import { PatternDraftQueue } from './pattern-draft-queue.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' @@ -46,7 +45,6 @@ export function Pattern(designConfig = {}) { __addNonEnumProp(this, 'Path', Path) __addNonEnumProp(this, 'Snippet', Snippet) __addNonEnumProp(this, 'Attributes', Attributes) - __addNonEnumProp(this, 'macros', {}) __addNonEnumProp(this, '__initialized', false) __addNonEnumProp(this, 'config.parts', {}) __addNonEnumProp(this, 'config.resolvedDependencies', {}) @@ -92,98 +90,14 @@ Pattern.prototype.addPart = function (part, resolveImmediately = true) { */ Pattern.prototype.draft = function () { this.__init() - this.draftQueue = new PatternDraftQueue(this) - this.__runHooks('preDraft') - // Keep container for drafted parts fresh - this.parts = [] - - // Iterate over the provided sets of settings (typically just one) - for (const set in this.settings) { - this.activeSet = set - this.setStores[set] = this.__createSetStore() - this.setStores[set].log.debug(`Initialized store for set ${set}`) - this.__runHooks('preSetDraft') - this.setStores[set].log.debug(`📐 Drafting pattern for set ${set}`) - - // Create parts container - this.parts[set] = {} - - // Handle snap for pct options - this.__loadAbsoluteOptionsSet(set) - - this.draftQueue.start() - while (this.draftQueue.hasNext()) { - this.createPartForSet(this.draftQueue.next(), set) - } - this.__runHooks('postSetDraft') - } - this.__runHooks('postDraft') + new PatternDrafter(this).draft() return this } -Pattern.prototype.createPartForSet = function (partName, set = 0) { - // gotta protect against attacks - if (set === '__proto__') { - throw new Error('malicious attempt at altering Object.prototype. Stopping action') - } - // 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.config.inject[partName] === 'string') { - this.setStores[set].log.debug( - `Creating part \`${partName}\` from part \`${this.config.inject[partName]}\`` - ) - try { - this.parts[set][partName].__inject(this.parts[set][this.config.inject[partName]]) - } catch (err) { - this.setStores[set].log.error([ - `Could not inject part \`${this.config.inject[partName]}\` into part \`${partName}\``, - err, - ]) - } - } - if (this.__needs(partName, set)) { - // Draft part - const result = this.draftPartForSet(partName, set) - if (typeof result !== 'undefined') this.parts[set][partName] = result - // 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.config.parts?.[partName]?.draft === 'function') { - this.activePart = partName - this.setStores[set].set('activePart', partName) - try { - this.__runHooks('prePartDraft') - const result = this.config.parts[partName].draft(this.parts[set][partName].shorthand()) - if (!this.__wants(partName, set)) { - result.hide() - } - this.__runHooks('postPartDraft') - if (typeof result === 'undefined') { - this.setStores[set].log.error( - `Result of drafting part ${partName} was undefined. Did you forget to return the part?` - ) - } - 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` - ) + this.__init() + return new PatternDrafter(this).draftPartForSet(partName, set) } /** @@ -323,39 +237,6 @@ Pattern.prototype.__applySettings = function (sets) { 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 - part.stack = this.config.parts[name]?.stack || name - part.context = { - parts: this.parts[set], - config: this.config, - settings: this.settings[set], - store: this.setStores[set], - macros: this.plugins.macros, - } - - if (this.settings[set]?.partClasses) { - part.attr('class', this.settings[set].partClasses) - } - - for (const macro in this.plugins.macros) { - part[__macroName(macro)] = this.plugins.macros[macro] - } - - return part -} - /** * Initializes the pattern coniguration and settings * @@ -426,34 +307,6 @@ Pattern.prototype.__isStackHidden = function (stackName) { return true } -/** - * 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 - ) - this.setStores[set].log.debug( - `🧲 Snapped __${optionName}__ to \`${this.settings[set].absoluteOptions[optionName]}\` for set __${set}__` - ) - } - } - - return this -} - /** * Loads data from the design config into the store * @@ -576,45 +429,6 @@ Pattern.prototype.__runHooks = function (hookName, data = false) { } } -/** - * 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]) - // Handle units-specific config - Side-step immutability for the snap conf - let snapConf = conf.snap - if (!Array.isArray(snapConf) && snapConf.metric && snapConf.imperial) - snapConf = snapConf[this.settings[set].units] - // Simple steps - if (typeof snapConf === 'number') return Math.ceil(abs / snapConf) * snapConf - // List of snaps - if (Array.isArray(snapConf) && snapConf.length > 1) { - for (const snap of snapConf - .sort((a, b) => a - b) - .map((snap, i) => { - const margin = - i < snapConf.length - 1 - ? (snapConf[Number(i) + 1] - snap) / 2 // Look forward - : (snap - snapConf[i - 1]) / 2 // Final snap, look backward - - return { - min: snap - margin, - max: snap + Number(margin), - snap, - } - })) - if (abs <= snap.max && abs >= snap.min) return snap.snap - } - - return abs -} - /** * Determines whether a part is wanted, depending on the 'only' setting and the configured dependencies * diff --git a/packages/core/src/pattern/pattern-draft-handler.mjs b/packages/core/src/pattern/pattern-draft-handler.mjs deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/core/src/pattern/pattern-drafter.mjs b/packages/core/src/pattern/pattern-drafter.mjs new file mode 100644 index 00000000000..4e9c6d8379d --- /dev/null +++ b/packages/core/src/pattern/pattern-drafter.mjs @@ -0,0 +1,227 @@ +import { PatternDraftQueue } from './pattern-draft-queue.mjs' +import { Part } from '../part.mjs' +import { __macroName } from '../utils.mjs' + +export function PatternDrafter(pattern) { + this.pattern = pattern +} + +Object.defineProperty(PatternDrafter.prototype, 'activeSet', { + get: function () { + return this.pattern.activeSet + }, + set: function (newVal) { + this.pattern.activeSet = newVal + }, +}) + +Object.defineProperty(PatternDrafter.prototype, 'activePart', { + get: function () { + return this.pattern.activePart + }, + set: function (newVal) { + this.pattern.activePart = newVal + this.activeStore.set('activePart', newVal) + }, +}) + +/** + * Drafts this pattern, aka the raison d'etre of FreeSewing + * + * @return {object} this - The Pattern instance + */ +PatternDrafter.prototype.draft = function () { + this.pattern.draftQueue = new PatternDraftQueue(this.pattern) + this.pattern.__runHooks('preDraft') + // Keep container for drafted parts fresh + this.pattern.parts = [] + + // Iterate over the provided sets of settings (typically just one) + for (const set in this.pattern.settings) { + this.pattern.setStores[set] = this.pattern.__createSetStore() + this.__useSet(set) + + this.activeStore.log.debug(`Initialized store for set ${set}`) + this.pattern.__runHooks('preSetDraft') + this.activeStore.log.debug(`📐 Drafting pattern for set ${set}`) + + // Create parts container + this.pattern.parts[set] = {} + + // Handle snap for pct options + this.__loadAbsoluteOptionsSet(set) + + this.pattern.draftQueue.start() + while (this.pattern.draftQueue.hasNext()) { + const partName = this.pattern.draftQueue.next() + if (this.pattern.__needs(partName, set)) { + this.draftPartForSet(partName, set) + } else { + this.activeStore.log.debug(`Part \`${partName}\` is not needed. Skipping part`) + } + } + this.pattern.__runHooks('postSetDraft') + } + this.pattern.__runHooks('postDraft') +} + +PatternDrafter.prototype.draftPartForSet = function (partName, set) { + this.__useSet(set) + this.__createPartForSet(partName, set) + + const configPart = this.pattern.config.parts?.[partName] + if (typeof configPart?.draft !== 'function') { + this.activeStore.log.error( + `Unable to draft pattern part __${partName}__. Part.draft() is not callable` + ) + return + } + + this.activePart = partName + try { + this.pattern.__runHooks('prePartDraft') + const result = configPart.draft(this.pattern.parts[set][partName].shorthand()) + + if (typeof result === 'undefined') { + this.activeStore.log.error( + `Result of drafting part ${partName} was undefined. Did you forget to return the part?` + ) + } else { + if (!this.pattern.__wants(partName, set)) result.hide() + this.pattern.__runHooks('postPartDraft') + this.pattern.parts[set][partName] = result + } + return result + } catch (err) { + this.activeStore.log.error([`Unable to draft part \`${partName}\` (set ${set})`, err]) + } +} + +PatternDrafter.prototype.__createPartForSet = function (partName, set = 0) { + // gotta protect against attacks + if (set === '__proto__') { + throw new Error('malicious attempt at altering Object.prototype. Stopping action') + } + // Create parts + this.activeStore.log.debug(`📦 Creating part \`${partName}\` (set ${set})`) + this.pattern.parts[set][partName] = this.__createPartWithContext(partName, set) + + // Handle inject/inheritance + const parent = this.pattern.config.inject[partName] + if (typeof parent === 'string') { + this.activeStore.log.debug(`Creating part \`${partName}\` from part \`${parent}\``) + try { + this.pattern.parts[set][partName].__inject(this.pattern.parts[set][parent]) + } catch (err) { + this.activeStore.log.error([ + `Could not inject part \`${parent}\` into part \`${partName}\``, + err, + ]) + } + } +} + +PatternDrafter.prototype.__useSet = function (set = 0) { + this.activeSet = set + this.activeSettings = this.pattern.settings[set] + this.activeStore = this.pattern.setStores[set] +} +/** + * 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 + */ +PatternDrafter.prototype.__loadAbsoluteOptionsSet = function (set) { + for (const optionName in this.pattern.settings[set].options) { + const option = this.pattern.config.options[optionName] + if ( + typeof option !== 'undefined' && + typeof option.snap !== 'undefined' && + option.toAbs instanceof Function + ) { + this.pattern.settings[set].absoluteOptions[optionName] = this.__snappedPercentageOption( + optionName, + set + ) + this.pattern.setStores[set].log.debug( + `🧲 Snapped __${optionName}__ to \`${this.pattern.settings[set].absoluteOptions[optionName]}\` for set __${set}__` + ) + } + } + + return this +} + +/** + * 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 + */ +PatternDrafter.prototype.__snappedPercentageOption = function (optionName, set) { + const conf = this.pattern.config.options[optionName] + const abs = conf.toAbs(this.pattern.settings[set].options[optionName], this.pattern.settings[set]) + // 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.pattern.settings[set].units] + // Simple steps + if (typeof snapConf === 'number') return Math.ceil(abs / snapConf) * snapConf + // List of snaps + if (Array.isArray(snapConf) && snapConf.length > 1) { + for (const snap of snapConf + .sort((a, b) => a - b) + .map((snap, i) => { + const margin = + i < snapConf.length - 1 + ? (snapConf[Number(i) + 1] - snap) / 2 // Look forward + : (snap - snapConf[i - 1]) / 2 // Final snap, look backward + + return { + min: snap - margin, + max: snap + Number(margin), + snap, + } + })) + if (abs <= snap.max && abs >= snap.min) return snap.snap + } + + return abs +} + +/** + * 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 + */ +PatternDrafter.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.pattern.config.parts[name]?.stack || name + part.context = { + parts: this.pattern.parts[set], + config: this.pattern.config, + settings: this.pattern.settings[set], + store: this.pattern.setStores[set], + macros: this.pattern.plugins.macros, + } + + if (this.pattern.settings[set]?.partClasses) { + part.attr('class', this.pattern.settings[set].partClasses) + } + + for (const macro in this.pattern.plugins.macros) { + part[__macroName(macro)] = this.pattern.plugins.macros[macro] + } + + return part +} diff --git a/packages/core/tests/pattern-draft.test.mjs b/packages/core/tests/pattern-draft.test.mjs index fae87165224..bceaa10221f 100644 --- a/packages/core/tests/pattern-draft.test.mjs +++ b/packages/core/tests/pattern-draft.test.mjs @@ -33,18 +33,6 @@ describe('Pattern', () => { expect(count).to.equal(2) }) }) - describe('Pattern.createPartForSet()', () => { - it('Should not allow malicious assignment to Object.prototype', () => { - const objProto = Object.prototype - const Pattern = new Design() - const pattern = new Pattern() - - expect(() => pattern.createPartForSet('part', '__proto__')).to.throw( - 'malicious attempt at altering Object.prototype. Stopping action' - ) - expect(objProto).to.equal(Object.prototype) - }) - }) it('Should check whether a part is needed', () => { const partA = { name: 'test.partA', diff --git a/packages/core/tests/pattern-other.test.mjs b/packages/core/tests/pattern-other.test.mjs index c15c7ce07fa..7b7a51dde9c 100644 --- a/packages/core/tests/pattern-other.test.mjs +++ b/packages/core/tests/pattern-other.test.mjs @@ -61,9 +61,7 @@ describe('Pattern', () => { const design = new Design({ parts: [test, you] }) const pattern = new design({ only: ['you'] }) pattern.draft() - expect(pattern.setStores[0].logs.debug).to.include( - 'Part `test` is not needed. Skipping draft and setting hidden to `true`' - ) + expect(pattern.setStores[0].logs.debug).to.include('Part `test` is not needed. Skipping part') }) it('Should return the initialized config', () => { From 13ec5e43e535c2371652f5c22afd2ede4de0d4c0 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Tue, 18 Apr 2023 00:00:25 -0400 Subject: [PATCH 06/10] refactor (core) reorganize method groupings in Pattern --- packages/core/src/pattern/index.mjs | 311 +++++++++--------- packages/core/src/pattern/pattern-config.mjs | 2 +- packages/core/src/pattern/pattern-drafter.mjs | 71 ++-- packages/core/src/pattern/pattern-sampler.mjs | 13 - packages/core/tests/hooks.test.mjs | 2 +- 5 files changed, 198 insertions(+), 201 deletions(-) diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs index 4a37e775321..b4079e74dd3 100644 --- a/packages/core/src/pattern/index.mjs +++ b/packages/core/src/pattern/index.mjs @@ -1,13 +1,9 @@ import { Attributes } from '../attributes.mjs' -import { __addNonEnumProp, __macroName } from '../utils.mjs' -import { Part } from '../part.mjs' -import { Stack } from '../stack.mjs' +import { __addNonEnumProp } from '../utils.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' @@ -40,7 +36,6 @@ export function Pattern(designConfig = {}) { __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) @@ -59,6 +54,10 @@ export function Pattern(designConfig = {}) { // PUBLIC METHODS // ////////////////////////////////////////////// +/////////// +// Setup // +/////////// + /** * Allows adding parts to the config at runtime * @@ -83,6 +82,49 @@ Pattern.prototype.addPart = function (part, resolveImmediately = true) { return this } +/** + * Return the initialized configuration + * + * @return {object} config - The initialized config + */ +Pattern.prototype.getConfig = function () { + return this.__init().config +} + +////////////////////////////// +// Plugin and Hook Handling // +////////////////////////////// + +/** + * 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) + + 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) + + return this +} + +////////////// +// Drafting // +////////////// /** * Drafts this pattern, aka the raison d'etre of FreeSewing * @@ -100,14 +142,9 @@ Pattern.prototype.draftPartForSet = function (partName, set) { return new PatternDrafter(this).draftPartForSet(partName, set) } -/** - * Return the initialized configuration - * - * @return {object} config - The initialized config - */ -Pattern.prototype.getConfig = function () { - return this.__init().config -} +/////////////// +// Rendering // +/////////////// /** * Renders the pattern to SVG @@ -127,13 +164,26 @@ Pattern.prototype.getRenderProps = function () { return new PatternRenderer(this).getRenderProps() } +////////////// +// Sampling // +////////////// + /** * Handles pattern sampling * * @return {object} this - The Pattern instance */ Pattern.prototype.sample = function () { - return new PatternSampler(this).sample() + this.__init() + const sampleSetting = this.settings[0].sample + if (sampleSetting.type === 'option') { + return this.sampleOption(sampleSetting.option) + } else if (sampleSetting.type === 'measurement') { + return this.sampleMeasurement(sampleSetting.measurement) + } else if (sampleSetting.type === 'models') { + return this.sampleModels(sampleSetting.models, sampleSetting.focus || false) + } + return this.draft() } /** @@ -163,50 +213,13 @@ Pattern.prototype.sampleOption = function (optionName) { return new PatternSampler(this).sampleOption(optionName) } -/** - * 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) - - 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) - - 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 -} +/////////// +// Setup // +/////////// /** * Merges (sets of) settings with the default settings @@ -237,6 +250,20 @@ Pattern.prototype.__applySettings = function (sets) { return this } +/** + * Creates a store for a set (of settings) + * + * @private + * @return {Store} store - A new store populated with relevant data/methods + */ +Pattern.prototype.__createSetStore = function () { + const store = new Store() + store.set('data', this.store.data) + store.extend([...this.plugins.__storeMethods]) + + return store +} + /** * Initializes the pattern coniguration and settings * @@ -258,67 +285,32 @@ Pattern.prototype.__init = function () { * 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 + // Resolve parts + this.designConfig.parts.forEach((p) => this.__configResolver.addPart(p)) - this.plugins.loadConfigPlugins(this.config, this.settings) // Loads plugins + // Print final part distances. + this.__configResolver.logPartDistances() + + // get the config from the resolver + this.config = this.__configResolver.asConfig() + + // load resolved plugins + this.plugins.loadConfigPlugins(this.config, this.settings) + + // Make config data available in store + if (this.designConfig.data) this.store.set('data', this.designConfig.data) + + // Merges default options with user provided ones + this.__loadOptionDefaults() this.store.log.info(`Pattern initialized. Draft order is: ${this.config.draftOrder.join(', ')}`) + this.__runHooks('postInit') - this.__initialized = true return this } -/** - * 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 - if (Array.isArray(this.settings[this.activeSet || 0].only)) { - if (this.settings[this.activeSet || 0].only.includes(partName)) return partHidden - } - if (this.config.partHide?.[partName]) return true - - return partHidden -} - -/** - * 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 - } - - return true -} - -/** - * 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) - - return this -} - /** * Merges defaults for options with user-provided options * @@ -353,6 +345,67 @@ Pattern.prototype.__loadOptionDefaults = function () { return this } +/////////// +// Hooks // +/////////// + +/** + * 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 + */ +Pattern.prototype.__runHooks = function (hookName, data = false) { + if (data === false) data = this + let hooks = this.plugins.hooks[hookName] + if (hooks.length > 0) { + this.store.log.debug(`Running \`${hookName}\` hooks`) + for (let hook of hooks) { + hook.method(data, hook.data) + } + } +} + +/////////////////////// +// Config Evaluation // +/////////////////////// + +/** + * 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 + if (Array.isArray(this.settings[this.activeSet || 0].only)) { + if (this.settings[this.activeSet || 0].only.includes(partName)) return partHidden + } + if (this.config.partHide?.[partName]) return true + + return partHidden +} + +/** + * 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 + } + + return true +} + /** * Determines whether a part is needed, depending on the 'only' setting and the configured dependencies * @@ -385,50 +438,6 @@ Pattern.prototype.__needs = function (partName, set = 0) { return false } -/** - * 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 -} - -/** - * 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 -} - -/** - * 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 - */ -Pattern.prototype.__runHooks = function (hookName, data = false) { - if (data === false) data = this - let hooks = this.plugins.hooks[hookName] - if (hooks.length > 0) { - this.store.log.debug(`Running \`${hookName}\` hooks`) - for (let hook of hooks) { - hook.method(data, hook.data) - } - } -} - /** * Determines whether a part is wanted, depending on the 'only' setting and the configured dependencies * diff --git a/packages/core/src/pattern/pattern-config.mjs b/packages/core/src/pattern/pattern-config.mjs index 69659d62fd0..e0800eb0325 100644 --- a/packages/core/src/pattern/pattern-config.mjs +++ b/packages/core/src/pattern/pattern-config.mjs @@ -66,7 +66,7 @@ export function PatternConfig(pattern) { const DISTANCE_DEBUG = false //////////////////// -// PUBLIC METHODs // +// PUBLIC METHODS // //////////////////// /** diff --git a/packages/core/src/pattern/pattern-drafter.mjs b/packages/core/src/pattern/pattern-drafter.mjs index 4e9c6d8379d..806abebbd3e 100644 --- a/packages/core/src/pattern/pattern-drafter.mjs +++ b/packages/core/src/pattern/pattern-drafter.mjs @@ -121,11 +121,39 @@ PatternDrafter.prototype.__createPartForSet = function (partName, set = 0) { } } -PatternDrafter.prototype.__useSet = function (set = 0) { - this.activeSet = set - this.activeSettings = this.pattern.settings[set] - this.activeStore = this.pattern.setStores[set] +/** + * 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 + */ +PatternDrafter.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.pattern.config.parts[name]?.stack || name + part.context = { + parts: this.pattern.parts[set], + config: this.pattern.config, + settings: this.pattern.settings[set], + store: this.pattern.setStores[set], + macros: this.pattern.plugins.macros, + } + + if (this.pattern.settings[set]?.partClasses) { + part.attr('class', this.pattern.settings[set].partClasses) + } + + for (const macro in this.pattern.plugins.macros) { + part[__macroName(macro)] = this.pattern.plugins.macros[macro] + } + + return part } + /** * Generates an array of settings.absoluteOptions objects for sampling a list option * @@ -193,35 +221,8 @@ PatternDrafter.prototype.__snappedPercentageOption = function (optionName, set) return abs } -/** - * 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 - */ -PatternDrafter.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.pattern.config.parts[name]?.stack || name - part.context = { - parts: this.pattern.parts[set], - config: this.pattern.config, - settings: this.pattern.settings[set], - store: this.pattern.setStores[set], - macros: this.pattern.plugins.macros, - } - - if (this.pattern.settings[set]?.partClasses) { - part.attr('class', this.pattern.settings[set].partClasses) - } - - for (const macro in this.pattern.plugins.macros) { - part[__macroName(macro)] = this.pattern.plugins.macros[macro] - } - - return part +PatternDrafter.prototype.__useSet = function (set = 0) { + this.activeSet = set + this.activeSettings = this.pattern.settings[set] + this.activeStore = this.pattern.setStores[set] } diff --git a/packages/core/src/pattern/pattern-sampler.mjs b/packages/core/src/pattern/pattern-sampler.mjs index 5ccc6baec43..01ee0b527ed 100644 --- a/packages/core/src/pattern/pattern-sampler.mjs +++ b/packages/core/src/pattern/pattern-sampler.mjs @@ -2,19 +2,6 @@ export function PatternSampler(pattern) { this.pattern = pattern } -PatternSampler.prototype.sample = function () { - this.pattern.__init() - const sampleSetting = this.pattern.settings[0].sample - if (sampleSetting.type === 'option') { - return this.sampleOption(sampleSetting.option) - } else if (sampleSetting.type === 'measurement') { - return this.sampleMeasurement(sampleSetting.measurement) - } else if (sampleSetting.type === 'models') { - return this.sampleModels(sampleSetting.models, sampleSetting.focus || false) - } - return this.pattern -} - /** * Handles measurement sampling * diff --git a/packages/core/tests/hooks.test.mjs b/packages/core/tests/hooks.test.mjs index 20b24fe2624..1985f840ff4 100644 --- a/packages/core/tests/hooks.test.mjs +++ b/packages/core/tests/hooks.test.mjs @@ -6,7 +6,7 @@ const expect = chai.expect describe('Hooks', () => { it('Should contain all hooks', () => { const pattern = new Pattern() - const h = pattern.hooks + const h = pattern.plugins.hooks const test = { preInit: [], postInit: [], From 8ccab0df2156508272a42e3abe40b2b7133395df Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Tue, 18 Apr 2023 18:47:49 -0400 Subject: [PATCH 07/10] chore (core) cleanup --- packages/core/src/pattern/index.mjs | 2 +- packages/core/src/pattern/pattern-drafter.mjs | 28 ++---- .../core/src/pattern/pattern-renderer.mjs | 92 +++++++++---------- packages/core/src/utils.mjs | 6 +- packages/core/tests/pattern-other.test.mjs | 3 +- packages/core/tests/pattern-renderer.test.mjs | 39 ++++++++ packages/core/tests/stacks.test.mjs | 2 +- packages/core/tests/utils.test.mjs | 2 +- 8 files changed, 97 insertions(+), 77 deletions(-) create mode 100644 packages/core/tests/pattern-renderer.test.mjs diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs index b4079e74dd3..41e95cf02d8 100644 --- a/packages/core/src/pattern/index.mjs +++ b/packages/core/src/pattern/index.mjs @@ -9,7 +9,7 @@ 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 { PatternPlugins } from './pattern-plugins.mjs' import { PatternRenderer } from './pattern-renderer.mjs' import cloneDeep from 'lodash.clonedeep' diff --git a/packages/core/src/pattern/pattern-drafter.mjs b/packages/core/src/pattern/pattern-drafter.mjs index 806abebbd3e..da6f6ef87bb 100644 --- a/packages/core/src/pattern/pattern-drafter.mjs +++ b/packages/core/src/pattern/pattern-drafter.mjs @@ -6,25 +6,6 @@ export function PatternDrafter(pattern) { this.pattern = pattern } -Object.defineProperty(PatternDrafter.prototype, 'activeSet', { - get: function () { - return this.pattern.activeSet - }, - set: function (newVal) { - this.pattern.activeSet = newVal - }, -}) - -Object.defineProperty(PatternDrafter.prototype, 'activePart', { - get: function () { - return this.pattern.activePart - }, - set: function (newVal) { - this.pattern.activePart = newVal - this.activeStore.set('activePart', newVal) - }, -}) - /** * Drafts this pattern, aka the raison d'etre of FreeSewing * @@ -66,6 +47,10 @@ PatternDrafter.prototype.draft = function () { } PatternDrafter.prototype.draftPartForSet = function (partName, set) { + // gotta protect against attacks + if (set === '__proto__') { + throw new Error('malicious attempt at altering Object.prototype. Stopping action') + } this.__useSet(set) this.__createPartForSet(partName, set) @@ -77,7 +62,8 @@ PatternDrafter.prototype.draftPartForSet = function (partName, set) { return } - this.activePart = partName + this.pattern.activePart = partName + this.activeStore.set('activePart', partName) try { this.pattern.__runHooks('prePartDraft') const result = configPart.draft(this.pattern.parts[set][partName].shorthand()) @@ -222,7 +208,7 @@ PatternDrafter.prototype.__snappedPercentageOption = function (optionName, set) } PatternDrafter.prototype.__useSet = function (set = 0) { - this.activeSet = set + this.pattern.activeSet = set this.activeSettings = this.pattern.settings[set] this.activeStore = this.pattern.setStores[set] } diff --git a/packages/core/src/pattern/pattern-renderer.mjs b/packages/core/src/pattern/pattern-renderer.mjs index 6215e0c5543..071553a9341 100644 --- a/packages/core/src/pattern/pattern-renderer.mjs +++ b/packages/core/src/pattern/pattern-renderer.mjs @@ -36,38 +36,36 @@ PatternRenderer.prototype.getRenderProps = function () { autoLayout: this.pattern.autoLayout, settings: this.pattern.settings, parts: [], + stacks: {}, } - for (const set of this.pattern.parts) { - const setParts = {} - for (let p in set) { - if (!set[p].hidden) { - setParts[p] = { - ...set[p].asProps(), - store: this.pattern.setStores[set[p].set], + for (const partSet of this.pattern.parts) { + const setPartProps = {} + for (let partName in partSet) { + const part = partSet[partName] + if (!part.hidden) { + setPartProps[partName] = { + ...partSet[partName].asProps(), + store: this.pattern.setStores[part.set], } - } else if (this.pattern.setStores[set.set]) { - this.pattern.setStores[set.set].log.info( - `Part${p} is hidden in set ${set.set}. Not adding to render props` + } else if (this.pattern.setStores[part.set]) { + this.pattern.setStores[part.set].log.info( + `Part ${partName} is hidden in set ${part.set}. Not adding to render props` ) } } - props.parts.push(setParts) + props.parts.push(setPartProps) } - props.stacks = {} + for (let s in this.pattern.stacks) { if (!this.pattern.__isStackHidden(s)) { props.stacks[s] = this.pattern.stacks[s].asProps() } else this.pattern.store.log.info(`Stack ${s} is hidden. Skipping in render props.`) } + props.logs = { pattern: this.pattern.store.logs, - sets: this.pattern.setStores.map((store) => ({ - debug: store.logs.debug, - info: store.logs.info, - error: store.logs.error, - warning: store.logs.warning, - })), + sets: this.pattern.setStores.map((store) => store.logs), } this.svg.__runHooks('postRender') @@ -107,7 +105,7 @@ PatternRenderer.prototype.__stack = function () { */ PatternRenderer.prototype.__pack = function () { this.pattern.__runHooks('preLayout') - const { settings, setStores, parts } = this.pattern + const { settings, setStores, activeSet } = this.pattern for (const set in settings) { if (setStores[set].logs.error.length > 0) { setStores[set].log.warning(`One or more errors occured. Not packing pattern parts`) @@ -123,39 +121,37 @@ PatternRenderer.prototype.__pack = function () { stack.attributes.remove('transform') if (!this.pattern.__isStackHidden(key)) { stack.home() - if (settings[0].layout === true) + if (settings[activeSet].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 (settings[activeSet].layout === true) { + // some plugins will add a width constraint to the settings, but we can safely pass undefined if not + let size = pack(bins, { inPlace: true, maxWidth: settings[0].maxWidth }) + this.autoLayout.width = size.width + this.autoLayout.height = size.height + + for (let bin of bins) { + let stack = this.stacks[bin.id] + this.autoLayout.stacks[bin.id] = { + move: { + x: bin.x + stack.layout.move.x, + y: bin.y + stack.layout.move.y, + }, } } } - if (settings[0].layout === true) { - // some plugins will add a width constraint to the settings, but we can safely pass undefined if not - let size = pack(bins, { inPlace: true, maxWidth: settings[0].maxWidth }) - 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 settings[0].layout === 'object') { - this.width = settings[0].layout.width - this.height = settings[0].layout.height - for (let stackId of Object.keys(settings[0].layout.stacks)) { - // Some parts are added by late-stage plugins - if (this.stacks[stackId]) { - let transforms = settings[this.activeStack || 0].layout.stacks[stackId] - this.stacks[stackId].generateTransform(transforms) - } + + const packedLayout = + typeof settings[activeSet].layout === 'object' ? settings[activeSet].layout : this.autoLayout + + this.width = packedLayout.width + this.height = packedLayout.height + for (let stackId of Object.keys(packedLayout.stacks)) { + // Some parts are added by late-stage plugins + if (this.stacks[stackId]) { + let transforms = packedLayout.stacks[stackId] + this.stacks[stackId].generateTransform(transforms) } } diff --git a/packages/core/src/utils.mjs b/packages/core/src/utils.mjs index 30840c42b4e..aea6af3d686 100644 --- a/packages/core/src/utils.mjs +++ b/packages/core/src/utils.mjs @@ -320,7 +320,7 @@ export const generateStackTransform = ( // add the scaling to the transforms if (scaleX + scaleY < 2) { - transforms.push(`scale(${scaleX} ${scaleY})`) + transforms.push(`scale(${scaleX}, ${scaleY})`) } if (rotate) { @@ -331,11 +331,11 @@ export const generateStackTransform = ( } // add the rotation around the center to the transforms - transforms.push(`rotate(${rotate} ${center.x} ${center.y})`) + transforms.push(`rotate(${rotate}, ${center.x}, ${center.y})`) } // put the translation before any other transforms to avoid having to make complex calculations once the matrix has been rotated or scaled - if (xTotal !== 0 || yTotal !== 0) transforms.unshift(`translate(${xTotal} ${yTotal})`) + if (xTotal !== 0 || yTotal !== 0) transforms.unshift(`translate(${xTotal}, ${yTotal})`) return { transform: transforms.join(' '), diff --git a/packages/core/tests/pattern-other.test.mjs b/packages/core/tests/pattern-other.test.mjs index 7b7a51dde9c..46fd350cdda 100644 --- a/packages/core/tests/pattern-other.test.mjs +++ b/packages/core/tests/pattern-other.test.mjs @@ -159,7 +159,6 @@ describe('Pattern', () => { expect(pattern.setStores[0].logs.error[0][0]).to.equal('Unable to draft part `test` (set 0)') }) - // FIXME: Add assertions here it('Handle layout object', () => { const part = { name: 'test', @@ -174,7 +173,7 @@ describe('Pattern', () => { layout: { stacks: { test: { flipX: true } }, width: 300, height: 400 }, }) const props = pattern.draft().getRenderProps() - expect(props.stacks.test.attributes.get('transform')).to.equal('scale(-1 1)') + expect(props.stacks.test.attributes.get('transform')).to.equal('scale(-1, 1)') expect(props.width).to.equal(300) expect(props.height).to.equal(400) }) diff --git a/packages/core/tests/pattern-renderer.test.mjs b/packages/core/tests/pattern-renderer.test.mjs new file mode 100644 index 00000000000..1888363d577 --- /dev/null +++ b/packages/core/tests/pattern-renderer.test.mjs @@ -0,0 +1,39 @@ +import chai from 'chai' +import { Design } from '../src/index.mjs' + +const expect = chai.expect + +describe('Pattern Rendering', () => { + describe('Pattern.prototype.getRenderProps()', () => { + describe('Hidden parts and stacks', () => { + const part = { + name: 'test', + draft: ({ part }) => { + part.hide() + return part + }, + } + + const design = new Design({ parts: [part] }) + const pattern = new design({}) + const props = pattern.draft().getRenderProps() + + it('Should not include hidden parts', () => { + expect(props.parts[0]).not.to.have.property('test') + }) + it('Should log that it has skipped a hidden part', () => { + expect(props.logs.sets[0].info).to.include( + 'Part test is hidden in set 0. Not adding to render props' + ) + }) + it('Should not include hidden stacks', () => { + expect(props.stacks).not.to.have.property('test') + }) + it('Should log that it has skipped a hidden stack', () => { + expect(props.logs.pattern.info).to.include( + 'Stack test is hidden. Skipping in render props.' + ) + }) + }) + }) +}) diff --git a/packages/core/tests/stacks.test.mjs b/packages/core/tests/stacks.test.mjs index 416dfc45743..f1b67b25ad1 100644 --- a/packages/core/tests/stacks.test.mjs +++ b/packages/core/tests/stacks.test.mjs @@ -170,7 +170,7 @@ describe('Stacks', () => { }, }) expect(pattern.stacks.test.attributes.list.transform.length).to.equal(1) - expect(pattern.stacks.test.attributes.list.transform[0]).to.equal('translate(10 20)') + expect(pattern.stacks.test.attributes.list.transform[0]).to.equal('translate(10, 20)') }) }) diff --git a/packages/core/tests/utils.test.mjs b/packages/core/tests/utils.test.mjs index 40ac8bf6467..bd0817f0ee6 100644 --- a/packages/core/tests/utils.test.mjs +++ b/packages/core/tests/utils.test.mjs @@ -492,6 +492,6 @@ describe('Utils', () => { const pattern = new design() const props = pattern.draft().getRenderProps() const transform = generateStackTransform(30, 60, 90, true, true, props.stacks.test) - expect(transform.transform).to.equal('translate(51 138) scale(-1 -1) rotate(90 10.5 39)') + expect(transform.transform).to.equal('translate(51, 138) scale(-1, -1) rotate(90, 10.5, 39)') }) }) From c6d4fc3aea4b0f00b02fd2a1b3293fc9549691a5 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Wed, 19 Apr 2023 11:30:32 -0400 Subject: [PATCH 08/10] fix (core) fix rounding in count sampling. add tests --- packages/core/src/pattern/pattern-sampler.mjs | 5 +- packages/core/tests/pattern-init.test.mjs | 34 +++- packages/core/tests/pattern-sample.test.mjs | 92 +++++++++ yarn.lock | 186 ++---------------- 4 files changed, 132 insertions(+), 185 deletions(-) diff --git a/packages/core/src/pattern/pattern-sampler.mjs b/packages/core/src/pattern/pattern-sampler.mjs index 01ee0b527ed..a65fb30a71e 100644 --- a/packages/core/src/pattern/pattern-sampler.mjs +++ b/packages/core/src/pattern/pattern-sampler.mjs @@ -193,6 +193,7 @@ PatternSampler.prototype.__optionSets = function (optionName) { } step = (option.max / factor - val) / stepFactor const base = this.__setBase() + const roundVal = typeof option.count !== 'undefined' || typeof option.mm !== 'undefined' for (let run = 1; run <= numberRuns; run++) { const settings = { ...base, @@ -202,11 +203,9 @@ PatternSampler.prototype.__optionSets = function (optionName) { idPrefix: `sample-${run}`, partClasses: `sample-${run}`, } - settings.options[optionName] = val + settings.options[optionName] = roundVal ? Math.ceil(val) : val sets.push(settings) val += step - if (typeof option.count !== 'undefined' || typeof option.mm !== 'undefined') - val = Math.round(val) } return sets diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index 718fa363988..b24ec6a2ebe 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -30,12 +30,6 @@ describe('Pattern', () => { expect(typeof pattern.Path).to.equal('function') expect(typeof pattern.Snippet).to.equal('function') expect(typeof pattern.Attributes).to.equal('function') - // expect(typeof pattern.__designParts).to.equal('object') - // expect(typeof pattern.config.inject).to.equal('object') - // expect(typeof pattern.config.directDependencies).to.equal('object') - // expect(typeof pattern.__resolvedDependencies).to.equal('object') - // expect(typeof pattern.__hide).to.equal('object') - // expect(Array.isArray(pattern.__draftOrder)).to.equal(true) expect(pattern.width).to.equal(0) expect(pattern.height).to.equal(0) expect(pattern.is).to.equal('') @@ -631,7 +625,7 @@ describe('Pattern', () => { expect(pattern.plugins.hooks.preRender).to.have.lengthOf(1) }) - it('Pattern.__init() should not load conditional plugin if condition is not mett', () => { + it('Pattern.__init() should not load conditional plugin if condition is not met', () => { const plugin = { name: 'example', version: 1, @@ -812,6 +806,32 @@ describe('Pattern', () => { expect(count).to.equal(2) }) + it('Pattern.__init() should not register the same method twice on one hook', () => { + function hookMethod() { + count++ + } + const plugin = { + name: 'test', + version: '0.1-test', + hooks: { + preDraft: [ + hookMethod, + hookMethod, + function () { + count++ + }, + ], + }, + } + const Pattern = new Design() + const pattern = new Pattern() + let count = 0 + pattern._draft = () => {} + pattern.use(plugin) + pattern.draft() + expect(count).to.equal(2) + }) + it('Should check whether created parts get the pattern context', () => { let partContext const plugin = { diff --git a/packages/core/tests/pattern-sample.test.mjs b/packages/core/tests/pattern-sample.test.mjs index f8b6af4ea58..5da639eeb23 100644 --- a/packages/core/tests/pattern-sample.test.mjs +++ b/packages/core/tests/pattern-sample.test.mjs @@ -31,6 +31,7 @@ describe('Pattern', () => { pattern.sample() expect(pattern.setStores.length).to.equal(10) expect(pattern.settings.length).to.equal(10) + expect(pattern.parts[0].test.paths.test.ops[1].to.y).to.equal(80) expect(pattern.parts[9].test.paths.test.ops[1].to.y).to.equal(320) }) @@ -60,9 +61,70 @@ describe('Pattern', () => { pattern.sample() expect(pattern.setStores.length).to.equal(10) expect(pattern.settings.length).to.equal(10) + expect(round(pattern.parts[0].test.paths.test.ops[1].to.y)).to.equal(round(0.05 * 0.9 * 400)) expect(round(pattern.parts[9].test.paths.test.ops[1].to.y)).to.equal(22) }) + it('Should sample a count option', () => { + const part = { + name: 'test', + measurements: ['head'], + options: { + size: { count: 2, min: 0, max: 6 }, + }, + draft: ({ Point, paths, Path, measurements, options, part }) => { + paths.test = new Path() + .move(new Point(0, 0)) + .line(new Point(0, measurements.head * options.size)) + + return part + }, + } + const Pattern = new Design({ parts: [part] }) + const pattern = new Pattern({ + measurements: { head: 400 }, + sample: { + type: 'option', + option: 'size', + }, + }) + pattern.sample() + expect(pattern.setStores.length).to.equal(7) + expect(pattern.settings.length).to.equal(7) + expect(round(pattern.parts[0].test.paths.test.ops[1].to.y)).to.equal(0) + expect(round(pattern.parts[6].test.paths.test.ops[1].to.y)).to.equal(2400) + }) + + it('Should not sample a count option more than 10 times', () => { + const part = { + name: 'test', + measurements: ['head'], + options: { + size: { count: 2, min: 0, max: 20 }, + }, + draft: ({ Point, paths, Path, measurements, options, part }) => { + paths.test = new Path() + .move(new Point(0, 0)) + .line(new Point(0, measurements.head * options.size)) + + return part + }, + } + const Pattern = new Design({ parts: [part] }) + const pattern = new Pattern({ + measurements: { head: 400 }, + sample: { + type: 'option', + option: 'size', + }, + }) + pattern.sample() + expect(pattern.setStores.length).to.equal(10) + expect(pattern.settings.length).to.equal(10) + expect(round(pattern.parts[0].test.paths.test.ops[1].to.y)).to.equal(0) + expect(round(pattern.parts[9].test.paths.test.ops[1].to.y)).to.equal(8000) + }) + it('Should sample a list option', () => { const part = { name: 'test', @@ -89,9 +151,39 @@ describe('Pattern', () => { pattern.sample() expect(pattern.setStores.length).to.equal(10) expect(pattern.settings.length).to.equal(10) + expect(pattern.parts[0].test.paths.test.ops[1].to.y).to.equal(40) expect(pattern.parts[9].test.paths.test.ops[1].to.y).to.equal(400) }) + it('Should sample a boolean option', () => { + const part = { + name: 'test', + measurements: ['head'], + options: { + reverse: { bool: true }, + }, + draft: ({ Point, paths, Path, measurements, options, part }) => { + const yFac = options.reverse ? -1 : 1 + paths.test = new Path().move(new Point(0, 0)).line(new Point(0, measurements.head * yFac)) + + return part + }, + } + const Pattern = new Design({ parts: [part] }) + const pattern = new Pattern({ + measurements: { head: 400 }, + sample: { + type: 'option', + option: 'reverse', + }, + }) + pattern.sample() + expect(pattern.setStores.length).to.equal(2) + expect(pattern.settings.length).to.equal(2) + expect(pattern.parts[0].test.paths.test.ops[1].to.y).to.equal(400) + expect(pattern.parts[1].test.paths.test.ops[1].to.y).to.equal(-400) + }) + it('Should sample a measurement', () => { const part = { name: 'test', diff --git a/yarn.lock b/yarn.lock index ed899b5c778..4b0b21925df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4468,7 +4468,7 @@ dependencies: "@types/estree" "*" -"@types/estree@*", "@types/estree@^1.0.0": +"@types/estree@*": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== @@ -4675,16 +4675,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16", "@types/react@^18.0.25", "@types/react@^18.0.8": - version "18.0.37" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.37.tgz#7a784e2a8b8f83abb04dc6b9ed9c9b4c0aee9be7" - integrity sha512-4yaZZtkRN3ZIQD3KSEwkfcik8s0SWV+82dlJot1AbGYHCzJkWP3ENBY6wYeDRmKZ6HkrgoGAmR2HqdwYGp6OEw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@>=16.0.0": +"@types/react@*", "@types/react@>=16", "@types/react@>=16.0.0", "@types/react@^18.0.25", "@types/react@^18.0.8": version "18.0.37" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.37.tgz#7a784e2a8b8f83abb04dc6b9ed9c9b4c0aee9be7" integrity sha512-4yaZZtkRN3ZIQD3KSEwkfcik8s0SWV+82dlJot1AbGYHCzJkWP3ENBY6wYeDRmKZ6HkrgoGAmR2HqdwYGp6OEw== @@ -4768,13 +4759,6 @@ resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== -"@types/yauzl@^2.9.1": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" - integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== - dependencies: - "@types/node" "*" - "@typescript-eslint/eslint-plugin@^5.42.0": version "5.47.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.47.0.tgz#dadb79df3b0499699b155839fd6792f16897d910" @@ -5960,7 +5944,7 @@ buble-jsx-only@^0.19.8: minimist "^1.2.0" regexpu-core "^4.5.4" -buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== @@ -5975,7 +5959,7 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^5.2.1, buffer@^5.5.0: +buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -7177,13 +7161,6 @@ cross-env@^7.0.2: dependencies: cross-spawn "^7.0.1" -cross-fetch@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -7925,11 +7902,6 @@ dev-ip@^1.0.1: resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0" integrity sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A== -devtools-protocol@0.0.1045489: - version "0.0.1045489" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1045489.tgz#f959ad560b05acd72d55644bc3fb8168a83abf28" - integrity sha512-D+PTmWulkuQW4D1NTiCRCFxF7pQPn0hgp4YyX4wAQ6xYXKOadSWPR3ENGDQ47MW/Ewc9v2rpC/UEEGahgBYpSQ== - dfa@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" @@ -8973,14 +8945,6 @@ estree-util-visit@^1.0.0: "@types/estree-jsx" "^0.0.1" "@types/unist" "^2.0.0" -estree-util-visit@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-1.2.1.tgz#8bc2bc09f25b00827294703835aabee1cc9ec69d" - integrity sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/unist" "^2.0.0" - estree-walker@^2.0.0, estree-walker@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" @@ -9179,17 +9143,6 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extract-zip@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -9275,13 +9228,6 @@ fclone@^1.0.11: resolved "https://registry.yarnpkg.com/fclone/-/fclone-1.0.11.tgz#10e85da38bfea7fc599341c296ee1d77266ee640" integrity sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw== -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== - dependencies: - pend "~1.2.0" - feed@4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" @@ -10330,7 +10276,7 @@ hast-util-embedded@^2.0.0: dependencies: hast-util-is-element "^2.0.0" -hast-util-from-html@1.0.1, hast-util-from-html@^1.0.1: +hast-util-from-html@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-1.0.1.tgz#907a723b3405921efb3339a12bc6481d8ba92925" integrity sha512-ehTy+4Lz1YAVF6enEuL9QFUHqJKRxAc8a7KACyhawY+YqTG5pLkrBHfykXELEy75N601fHDr36HIqCGSNxmgZw== @@ -10452,27 +10398,6 @@ hast-util-to-estree@^2.0.0: unist-util-position "^4.0.0" zwitch "^2.0.0" -hast-util-to-estree@^2.1.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-2.3.2.tgz#11ab0cd2e70ecf0305151af56e636b1cdfbba0bf" - integrity sha512-YYDwATNdnvZi3Qi84iatPIl1lWpXba1MeNrNbDfJfVzEBZL8uUmtR7mt7bxKBC8kuAuvb0bkojXYZzsNHyHCLg== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/unist" "^2.0.0" - comma-separated-tokens "^2.0.0" - estree-util-attach-comments "^2.0.0" - estree-util-is-identifier-name "^2.0.0" - hast-util-whitespace "^2.0.0" - mdast-util-mdx-expression "^1.0.0" - mdast-util-mdxjs-esm "^1.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^0.4.1" - unist-util-position "^4.0.0" - zwitch "^2.0.0" - hast-util-to-html@^8.0.0: version "8.0.3" resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz#4e37580872e143ea9ce0dba87918b19e4ea997e3" @@ -10778,7 +10703,7 @@ http2-wrapper@^1.0.0-beta.5.2: quick-lru "^5.1.1" resolve-alpn "^1.0.0" -https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -13057,24 +12982,6 @@ mdast-util-from-markdown@^1.0.0: unist-util-stringify-position "^3.0.0" uvu "^0.5.0" -mdast-util-from-markdown@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz#0214124154f26154a2b3f9d401155509be45e894" - integrity sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - decode-named-character-reference "^1.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" - mdast-util-frontmatter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-1.0.0.tgz#ef12469379782e4a0fd995fed60cc3b871e6c819" @@ -13311,20 +13218,6 @@ mdurl@^1.0.0, mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== -mdx-mermaid@^2.0.0-rc7: - version "2.0.0-rc7" - resolved "https://registry.yarnpkg.com/mdx-mermaid/-/mdx-mermaid-2.0.0-rc7.tgz#60e5ce3abf160723610d1212c1423541339f725e" - integrity sha512-AMy3138EsvcGwE4cGqUVytj4mLpJ3TJ2nek82+67Qi4GSOXoelAqmQudHSk26IsP091c3qX4d/4wi9CqLwFl9Q== - optionalDependencies: - estree-util-to-js "^1.1.0" - estree-util-visit "^1.2.0" - hast-util-from-html "^1.0.1" - hast-util-to-estree "^2.1.0" - mdast-util-from-markdown "^1.2.0" - mdast-util-mdx "^2.0.0" - micromark-extension-mdxjs "^1.0.0" - puppeteer "^18.0.0" - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -13385,7 +13278,7 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mermaid@^10.1.0: +mermaid@10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.1.0.tgz#6e40d5250174f4750ca6548e4ee00f6ae210855a" integrity sha512-LYekSMNJygI1VnMizAPUddY95hZxOjwZxr7pODczILInO0dhQKuhXeu4sargtnuTwCilSuLS7Uiq/Qn7HTVrmA== @@ -15709,11 +15602,6 @@ pegjs@^0.10.0: resolved "https://registry.yarnpkg.com/pegjs/-/pegjs-0.10.0.tgz#cf8bafae6eddff4b5a7efb185269eaaf4610ddbd" integrity sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow== -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -16330,11 +16218,6 @@ progress-stream@^2.0.0: speedometer "~1.0.0" through2 "~2.0.3" -progress@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - promise-all-reject-late@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2" @@ -16412,7 +16295,7 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -proxy-from-env@1.1.0, proxy-from-env@^1.1.0: +proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -16474,32 +16357,6 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" -puppeteer-core@18.2.1: - version "18.2.1" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-18.2.1.tgz#9b7827bb2bf478bb615e2c21425e4659555dc1fe" - integrity sha512-MRtTAZfQTluz3U2oU/X2VqVWPcR1+94nbA2V6ZrSZRVEwLqZ8eclZ551qGFQD/vD2PYqHJwWOW/fpC721uznVw== - dependencies: - cross-fetch "3.1.5" - debug "4.3.4" - devtools-protocol "0.0.1045489" - extract-zip "2.0.1" - https-proxy-agent "5.0.1" - proxy-from-env "1.1.0" - rimraf "3.0.2" - tar-fs "2.1.1" - unbzip2-stream "1.4.3" - ws "8.9.0" - -puppeteer@^18.0.0: - version "18.2.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-18.2.1.tgz#08967cd423efe511ee4c6e3a5c882ffaf2e6bbf3" - integrity sha512-7+UhmYa7wxPh2oMRwA++k8UGVDxh3YdWFB52r9C3tM81T6BU7cuusUSxImz0GEYSOYUKk/YzIhkQ6+vc0gHbxQ== - dependencies: - https-proxy-agent "5.0.1" - progress "2.0.3" - proxy-from-env "1.1.0" - puppeteer-core "18.2.1" - q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -17936,7 +17793,7 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -19149,7 +19006,7 @@ style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -style-to-object@^0.4.0, style-to-object@^0.4.1: +style-to-object@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.1.tgz#53cf856f7cf7f172d72939d9679556469ba5de37" integrity sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw== @@ -19388,7 +19245,7 @@ tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@2.1.1, tar-fs@^2.0.0, tar-fs@^2.1.1: +tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -19908,14 +19765,6 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unbzip2-stream@1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" - integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" @@ -20845,11 +20694,6 @@ write-pkg@4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" -ws@8.9.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e" - integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg== - ws@^7.3.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" @@ -21035,14 +20879,6 @@ yargs@^17.0.0, yargs@^17.3.0, yargs@^17.3.1, yargs@^17.6.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" From 5abd8e166bbfd80d8edf43c92866315c2288a0b6 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Wed, 19 Apr 2023 12:40:48 -0400 Subject: [PATCH 09/10] chore (core) document new classes [vercel skip] --- packages/core/src/pattern/pattern-drafter.mjs | 46 +++++++++---------- packages/core/src/pattern/pattern-plugins.mjs | 44 ++++++++++-------- .../core/src/pattern/pattern-renderer.mjs | 4 ++ packages/core/src/pattern/pattern-sampler.mjs | 32 +++++++------ 4 files changed, 67 insertions(+), 59 deletions(-) diff --git a/packages/core/src/pattern/pattern-drafter.mjs b/packages/core/src/pattern/pattern-drafter.mjs index da6f6ef87bb..8db3b11c0b7 100644 --- a/packages/core/src/pattern/pattern-drafter.mjs +++ b/packages/core/src/pattern/pattern-drafter.mjs @@ -2,6 +2,10 @@ import { PatternDraftQueue } from './pattern-draft-queue.mjs' import { Part } from '../part.mjs' import { __macroName } from '../utils.mjs' +/** + * A class to handle drafting a pattern + * @param {Pattern} pattern the pattern to draft + */ export function PatternDrafter(pattern) { this.pattern = pattern } @@ -32,6 +36,7 @@ PatternDrafter.prototype.draft = function () { // Handle snap for pct options this.__loadAbsoluteOptionsSet(set) + // draft all the parts for this set this.pattern.draftQueue.start() while (this.pattern.draftQueue.hasNext()) { const partName = this.pattern.draftQueue.next() @@ -46,6 +51,12 @@ PatternDrafter.prototype.draft = function () { this.pattern.__runHooks('postDraft') } +/** + * Draft and save a part for the given set of settings + * @param {String} partName the name of the part + * @param {number} set the index of the settings set + * @return {Part} the drafted part, which is also stored in the Pattern + */ PatternDrafter.prototype.draftPartForSet = function (partName, set) { // gotta protect against attacks if (set === '__proto__') { @@ -54,6 +65,7 @@ PatternDrafter.prototype.draftPartForSet = function (partName, set) { this.__useSet(set) this.__createPartForSet(partName, set) + // don't draft what can't be drafted const configPart = this.pattern.config.parts?.[partName] if (typeof configPart?.draft !== 'function') { this.activeStore.log.error( @@ -62,10 +74,12 @@ PatternDrafter.prototype.draftPartForSet = function (partName, set) { return } + // set the active part for use by hooks and such this.pattern.activePart = partName this.activeStore.set('activePart', partName) try { this.pattern.__runHooks('prePartDraft') + // draft const result = configPart.draft(this.pattern.parts[set][partName].shorthand()) if (typeof result === 'undefined') { @@ -73,8 +87,10 @@ PatternDrafter.prototype.draftPartForSet = function (partName, set) { `Result of drafting part ${partName} was undefined. Did you forget to return the part?` ) } else { + // hide if necessary if (!this.pattern.__wants(partName, set)) result.hide() this.pattern.__runHooks('postPartDraft') + // save the result this.pattern.parts[set][partName] = result } return result @@ -83,30 +99,6 @@ PatternDrafter.prototype.draftPartForSet = function (partName, set) { } } -PatternDrafter.prototype.__createPartForSet = function (partName, set = 0) { - // gotta protect against attacks - if (set === '__proto__') { - throw new Error('malicious attempt at altering Object.prototype. Stopping action') - } - // Create parts - this.activeStore.log.debug(`📦 Creating part \`${partName}\` (set ${set})`) - this.pattern.parts[set][partName] = this.__createPartWithContext(partName, set) - - // Handle inject/inheritance - const parent = this.pattern.config.inject[partName] - if (typeof parent === 'string') { - this.activeStore.log.debug(`Creating part \`${partName}\` from part \`${parent}\``) - try { - this.pattern.parts[set][partName].__inject(this.pattern.parts[set][parent]) - } catch (err) { - this.activeStore.log.error([ - `Could not inject part \`${parent}\` into part \`${partName}\``, - err, - ]) - } - } -} - /** * Instantiates a new Part instance and populates it with the pattern context * @@ -207,8 +199,12 @@ PatternDrafter.prototype.__snappedPercentageOption = function (optionName, set) return abs } +/** + * Sets the active set + * @param {Number} set the set to use + * @private + */ PatternDrafter.prototype.__useSet = function (set = 0) { this.pattern.activeSet = set - this.activeSettings = this.pattern.settings[set] this.activeStore = this.pattern.setStores[set] } diff --git a/packages/core/src/pattern/pattern-plugins.mjs b/packages/core/src/pattern/pattern-plugins.mjs index e41d6a5b78d..a8006abf0ec 100644 --- a/packages/core/src/pattern/pattern-plugins.mjs +++ b/packages/core/src/pattern/pattern-plugins.mjs @@ -11,6 +11,10 @@ export function getPluginName(plugin) { return toCheck.name || toCheck.plugin?.name || false } +/** + * A class for managing the plugins and lifecycle hooks of a pattern + * @param {Pattern} pattern the pattern to manage + */ export function PatternPlugins(pattern) { this.store = pattern.store @@ -21,21 +25,15 @@ export function PatternPlugins(pattern) { } /** - * Loads a plugin + * Loads the plugins that are part of the config * - * @param {object} plugin - The plugin to load - * @param {object} data - Any data to pass to the plugin - * @return {object} this - The Pattern instance + * @private + * @return {Pattern} this - The Pattern instance */ -PatternPlugins.prototype.use = function (plugin, data, settings = [{}]) { - const name = getPluginName(plugin) - if (!this.plugins?.[name]) - return plugin.plugin && plugin.condition - ? this.__useIf(plugin, data, settings) // Conditional plugin - : this.__loadPlugin(plugin, data) // Regular plugin - - this.store.log.info(`Plugin \`${name}\` was requested, but it's already loaded. Skipping.`) - +PatternPlugins.prototype.loadConfigPlugins = function (config, settings) { + if (!config.plugins) return this + for (const plugin in config.plugins) + this.use(config.plugins[plugin], config.plugins[plugin]?.data, settings) return this } @@ -58,15 +56,21 @@ PatternPlugins.prototype.on = function (hook, method, data) { } /** - * Loads the plugins that are part of the config + * Loads a plugin * - * @private - * @return {Pattern} this - The Pattern instance + * @param {object} plugin - The plugin to load + * @param {object} data - Any data to pass to the plugin + * @return {object} this - The Pattern instance */ -PatternPlugins.prototype.loadConfigPlugins = function (config, settings) { - if (!config.plugins) return this - for (const plugin in config.plugins) - this.use(config.plugins[plugin], config.plugins[plugin]?.data, settings) +PatternPlugins.prototype.use = function (plugin, data, settings = [{}]) { + const name = getPluginName(plugin) + if (!this.plugins?.[name]) + return plugin.plugin && plugin.condition + ? this.__useIf(plugin, data, settings) // Conditional plugin + : this.__loadPlugin(plugin, data) // Regular plugin + + this.store.log.info(`Plugin \`${name}\` was requested, but it's already loaded. Skipping.`) + return this } diff --git a/packages/core/src/pattern/pattern-renderer.mjs b/packages/core/src/pattern/pattern-renderer.mjs index 071553a9341..4e418b0f0f7 100644 --- a/packages/core/src/pattern/pattern-renderer.mjs +++ b/packages/core/src/pattern/pattern-renderer.mjs @@ -2,6 +2,10 @@ import { Svg } from '../svg.mjs' import { Stack } from '../stack.mjs' import pack from 'bin-pack-with-constraints' +/** + * A class for handling layout and rendering for a pattern + * @param {Pattern} pattern the pattern to layout or render + */ export function PatternRenderer(pattern) { this.pattern = pattern this.autoLayout = pattern.autoLayout diff --git a/packages/core/src/pattern/pattern-sampler.mjs b/packages/core/src/pattern/pattern-sampler.mjs index a65fb30a71e..07e5687765c 100644 --- a/packages/core/src/pattern/pattern-sampler.mjs +++ b/packages/core/src/pattern/pattern-sampler.mjs @@ -1,3 +1,7 @@ +/** + * A class for handling pattern sampling + * @param {Pattern} pattern the pattern that will be sampled + */ export function PatternSampler(pattern) { this.pattern = pattern } @@ -47,20 +51,6 @@ PatternSampler.prototype.sampleOption = function (optionName) { return this.pattern.draft() } -/** - * Returns the base/defaults to generate a set of settings - * - * @private - * @return {object} settings - The settings object - */ -PatternSampler.prototype.__setBase = function () { - return { - measurements: {}, - options: {}, - ...this.pattern.settings[0], - } -} - /** * Generates an array of settings.options objects for sampling a list or boolean option * @@ -210,3 +200,17 @@ PatternSampler.prototype.__optionSets = function (optionName) { return sets } + +/** + * Returns the base/defaults to generate a set of settings + * + * @private + * @return {object} settings - The settings object + */ +PatternSampler.prototype.__setBase = function () { + return { + measurements: {}, + options: {}, + ...this.pattern.settings[0], + } +} From da76f232740e1b3dbd50709f74f9d551cac11cbd Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Thu, 20 Apr 2023 10:07:43 -0400 Subject: [PATCH 10/10] add back in accidentally deleted method --- packages/core/src/pattern/index.mjs | 4 +-- packages/core/src/pattern/pattern-drafter.mjs | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs index 41e95cf02d8..71521b1b4c8 100644 --- a/packages/core/src/pattern/index.mjs +++ b/packages/core/src/pattern/index.mjs @@ -275,8 +275,8 @@ Pattern.prototype.__init = function () { this.__runHooks('preInit') // Say hello this.store.log.info( - `New \`${this.designConfig?.data?.name || 'No Name'}:` + - `${this.designConfig?.data?.version || 'No version'}\` ` + + `New \`${this.designConfig.data?.name || 'No Name'}:` + + `${this.designConfig.data?.version || 'No version'}\` ` + `pattern using \`@freesewing/core:${version}\`` ) diff --git a/packages/core/src/pattern/pattern-drafter.mjs b/packages/core/src/pattern/pattern-drafter.mjs index 8db3b11c0b7..fe85faed41a 100644 --- a/packages/core/src/pattern/pattern-drafter.mjs +++ b/packages/core/src/pattern/pattern-drafter.mjs @@ -99,6 +99,37 @@ PatternDrafter.prototype.draftPartForSet = function (partName, set) { } } +/** + * Create a part for the given set of settings. + * Handles injection + * @param {String} partName the name of the part to create + * @param {Number} set the settings index + * @private + */ +PatternDrafter.prototype.__createPartForSet = function (partName, set = 0) { + // gotta protect against attacks + if (set === '__proto__') { + throw new Error('malicious attempt at altering Object.prototype. Stopping action') + } + // Create parts + this.activeStore.log.debug(`📦 Creating part \`${partName}\` (set ${set})`) + this.pattern.parts[set][partName] = this.__createPartWithContext(partName, set) + + // Handle inject/inheritance + const parent = this.pattern.config.inject[partName] + if (typeof parent === 'string') { + this.activeStore.log.debug(`Creating part \`${partName}\` from part \`${parent}\``) + try { + this.pattern.parts[set][partName].__inject(this.pattern.parts[set][parent]) + } catch (err) { + this.activeStore.log.error([ + `Could not inject part \`${parent}\` into part \`${partName}\``, + err, + ]) + } + } +} + /** * Instantiates a new Part instance and populates it with the pattern context *