From 6dbe15a862a3baf6f0f4fecd673df38ee5ea3e84 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Thu, 16 Feb 2023 15:21:55 -0600 Subject: [PATCH 01/67] cutting layout renders at the most basic level --- .../workbench/exporting/export-handler.mjs | 2 +- .../components/workbench/layout/cut/index.mjs | 39 +++++++++++++++++++ .../workbench/layout/cut/settings.mjs | 24 +++++++++++- .../workbench/layout/draft/index.mjs | 2 +- .../plugin.mjs => layout-part-plugin.mjs} | 10 ++++- .../workbench/layout/print/index.mjs | 2 +- .../shared/components/wrappers/workbench.mjs | 4 +- 7 files changed, 76 insertions(+), 7 deletions(-) rename sites/shared/components/workbench/layout/{print/plugin.mjs => layout-part-plugin.mjs} (98%) diff --git a/sites/shared/components/workbench/exporting/export-handler.mjs b/sites/shared/components/workbench/exporting/export-handler.mjs index abd27ba6c22..4c8174c8083 100644 --- a/sites/shared/components/workbench/exporting/export-handler.mjs +++ b/sites/shared/components/workbench/exporting/export-handler.mjs @@ -1,7 +1,7 @@ import Worker from 'web-worker' import fileSaver from 'file-saver' import { themePlugin } from '@freesewing/plugin-theme' -import { pagesPlugin } from '../layout/print/plugin.mjs' +import { pagesPlugin } from '../layout/layout-part-plugin.mjs' import { capitalize } from 'shared/utils.mjs' export const exportTypes = { diff --git a/sites/shared/components/workbench/layout/cut/index.mjs b/sites/shared/components/workbench/layout/cut/index.mjs index 76478ec8bc1..a5bc7d602d5 100644 --- a/sites/shared/components/workbench/layout/cut/index.mjs +++ b/sites/shared/components/workbench/layout/cut/index.mjs @@ -1,15 +1,54 @@ import { useTranslation } from 'next-i18next' import { CutLayoutSettings } from './settings.mjs' +import { Draft } from '../draft/index.mjs' +import { fabricPlugin } from '../layout-part-plugin.mjs' +import { useEffect } from 'react' export const CutLayout = (props) => { const { t } = useTranslation(['workbench']) + // disable xray + useEffect(() => { + if (props.gist?._state?.xray?.enabled) props.updateGist(['_state', 'xray', 'enabled'], false) + }) + + const draft = props.draft + + // add the pages plugin to the draft + const layoutSettings = { + sheetWidth: 500, + sheetHeight: 1000, + ...props.gist?._state?.layout?.forCutting?.fabric, + } + + draft.use(fabricPlugin(layoutSettings)) + + let patternProps + try { + // draft the pattern + draft.draft() + patternProps = draft.getRenderProps() + } catch (err) { + console.log(err, props.gist) + } + const bgProps = { fill: 'url(#page)' } + let name = props.design.designConfig.data.name name = name.replace('@freesewing/', '') return (

{t('layoutThing', { thing: name }) + ': ' + t('forCutting')}

+
) } diff --git a/sites/shared/components/workbench/layout/cut/settings.mjs b/sites/shared/components/workbench/layout/cut/settings.mjs index 5f6840cc784..bddb9d47acb 100644 --- a/sites/shared/components/workbench/layout/cut/settings.mjs +++ b/sites/shared/components/workbench/layout/cut/settings.mjs @@ -1,7 +1,27 @@ -export const CutLayoutSettings = () => { +import { ClearIcon } from 'shared/components/icons.mjs' +import { useTranslation } from 'next-i18next' + +export const CutLayoutSettings = (props) => { + const { t } = useTranslation(['workbench']) + let fabric = props.draft?.setStores[0].get('fabric') + if (!fabric) return null + const { cols, rows, count } = fabric + return (
-

Fixme: Cut layout settings here

+
+ +
) } diff --git a/sites/shared/components/workbench/layout/draft/index.mjs b/sites/shared/components/workbench/layout/draft/index.mjs index 3f01508dba0..8e353305bce 100644 --- a/sites/shared/components/workbench/layout/draft/index.mjs +++ b/sites/shared/components/workbench/layout/draft/index.mjs @@ -18,7 +18,7 @@ export const Draft = (props) => { const svgRef = useRef(null) if (!patternProps) return null // keep a fresh copy of the layout because we might manipulate it without saving to the gist - let layout = draft.settings[0].layouts?.printingLayout || { + let layout = draft.settings[0].layouts?.[layoutType] || { ...patternProps.autoLayout, width: patternProps.width, height: patternProps.height, diff --git a/sites/shared/components/workbench/layout/print/plugin.mjs b/sites/shared/components/workbench/layout/layout-part-plugin.mjs similarity index 98% rename from sites/shared/components/workbench/layout/print/plugin.mjs rename to sites/shared/components/workbench/layout/layout-part-plugin.mjs index e3ab04139d4..b6403818a03 100644 --- a/sites/shared/components/workbench/layout/print/plugin.mjs +++ b/sites/shared/components/workbench/layout/layout-part-plugin.mjs @@ -50,6 +50,14 @@ export const pagesPlugin = ({ size = 'a4', ...settings }) => { return basePlugin({ ...settings, sheetWidth, sheetHeight }) } +export const fabricPlugin = (settings) => { + return basePlugin({ + ...settings, + partName: 'fabric', + responsiveColumns: false, + }) +} + /** check if there is anything to render on the given section of the svg so that we can skip empty pages */ const doScanForBlanks = (stacks, layout, x, y, w, h) => { let hasContent = false @@ -92,7 +100,7 @@ const doScanForBlanks = (stacks, layout, x, y, w, h) => { * sheetHeight: the height of the helper part * boundary: should the helper part calculate its boundary? * responsiveColumns: should the part make more columns if the pattern exceed its width? (for pages you want this, for fabric you don't) - * printStyle: hould the pages be rendered for printing or for screen viewing? + * printStyle: should the pages be rendered for printing or for screen viewing? * */ const basePlugin = ({ sheetWidth, diff --git a/sites/shared/components/workbench/layout/print/index.mjs b/sites/shared/components/workbench/layout/print/index.mjs index 8ec25143dfe..3c5412649e7 100644 --- a/sites/shared/components/workbench/layout/print/index.mjs +++ b/sites/shared/components/workbench/layout/print/index.mjs @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'next-i18next' import { PrintLayoutSettings } from './settings.mjs' import { Draft } from '../draft/index.mjs' -import { pagesPlugin } from './plugin.mjs' +import { pagesPlugin } from '../layout-part-plugin.mjs' import { handleExport, defaultPdfSettings, diff --git a/sites/shared/components/wrappers/workbench.mjs b/sites/shared/components/wrappers/workbench.mjs index ed84760bf76..1339e4ce37f 100644 --- a/sites/shared/components/wrappers/workbench.mjs +++ b/sites/shared/components/wrappers/workbench.mjs @@ -119,7 +119,9 @@ export const WorkbenchWrapper = ({ // Generate the draft here so we can pass it down to both the view and the options menu let draft = false - if (['draft', 'logs', 'test', 'printingLayout'].indexOf(gist._state?.view) !== -1) { + if ( + ['draft', 'logs', 'test', 'printingLayout', 'cuttingLayout'].indexOf(gist._state?.view) !== -1 + ) { gist.embed = true // get the appropriate layout for the view const layout = gist.layouts?.[gist._state.view] || gist.layout || true From 18a5dac39d8009880b9cec4f7b231efffd4204d0 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Sat, 18 Feb 2023 16:18:35 +0200 Subject: [PATCH 02/67] cutting layout allows setting fabric width and calculates fabric length --- .../workbench/inputs/measurement.mjs | 2 - .../components/workbench/layout/cut/index.mjs | 12 +-- .../workbench/layout/cut/settings.mjs | 80 +++++++++++++++++-- sites/shared/utils.mjs | 61 +++++++++----- 4 files changed, 121 insertions(+), 34 deletions(-) diff --git a/sites/shared/components/workbench/inputs/measurement.mjs b/sites/shared/components/workbench/inputs/measurement.mjs index ecbb9a228f2..88534a8de30 100644 --- a/sites/shared/components/workbench/inputs/measurement.mjs +++ b/sites/shared/components/workbench/inputs/measurement.mjs @@ -56,8 +56,6 @@ export const MeasurementInput = ({ m, gist, app, updateMeasurements, focus }) => ) // use this for better update efficiency - // FIXME: This breaks gist updates. - // See: https://github.com/freesewing/freesewing/issues/2281 const memoVal = useMemo(() => gist.measurements?.[m], [gist]) // track validity against the value and the units const valid = useMemo( diff --git a/sites/shared/components/workbench/layout/cut/index.mjs b/sites/shared/components/workbench/layout/cut/index.mjs index a5bc7d602d5..18a72ffbde2 100644 --- a/sites/shared/components/workbench/layout/cut/index.mjs +++ b/sites/shared/components/workbench/layout/cut/index.mjs @@ -3,6 +3,7 @@ import { CutLayoutSettings } from './settings.mjs' import { Draft } from '../draft/index.mjs' import { fabricPlugin } from '../layout-part-plugin.mjs' import { useEffect } from 'react' +import { measurementAsMm } from 'shared/utils.mjs' export const CutLayout = (props) => { const { t } = useTranslation(['workbench']) @@ -13,14 +14,15 @@ export const CutLayout = (props) => { }) const draft = props.draft + const isImperial = props.gist.units === 'imperial' + const gistSettings = props.gist?._state?.layout?.forCutting?.fabric || {} // add the pages plugin to the draft const layoutSettings = { - sheetWidth: 500, - sheetHeight: 1000, - ...props.gist?._state?.layout?.forCutting?.fabric, + sheetWidth: gistSettings.sheetWidth || measurementAsMm(isImperial ? 54 : 50, props.gist.units), + sheetHeight: + gistSettings.sheetHeight || measurementAsMm(isImperial ? 36 : 100, props.gist.units), } - draft.use(fabricPlugin(layoutSettings)) let patternProps @@ -38,7 +40,7 @@ export const CutLayout = (props) => { return (

{t('layoutThing', { thing: name }) + ': ' + t('forCutting')}

- + { +const FabricSizer = ({ gist, updateGist }) => { const { t } = useTranslation(['workbench']) - let fabric = props.draft?.setStores[0].get('fabric') - if (!fabric) return null - const { cols, rows, count } = fabric + const [val, setVal] = useState(500) + + useEffect(() => { + setVal(formatMm(gist._state?.layout?.forCutting?.fabric.sheetWidth || 500, gist.units, 'none')) + }, [gist]) + const setFabricWidth = (width) => {} + + // onChange + const update = useCallback( + (evt) => { + evt.stopPropagation() + let evtVal = evt.target.value + // set Val immediately so that the input reflects it + setVal(evtVal) + + let useVal = measurementAsMm(evtVal, gist.units) + // only set to the gist if it's valid + if (!isNaN(useVal)) { + updateGist(['_state', 'layout', 'forCutting', 'fabric', 'sheetWidth'], useVal) + } + }, + [gist.units] + ) + + return ( +
+
+ +
+
+ ) +} + +const useFabricLength = (isImperial, height) => { + // regular conversion from mm to inches or cm + const unit = isImperial ? 25.4 : 10 + // conversion from inches or cm to yards or meters + const fabricUnit = isImperial ? 36 : 100 + // for fabric, these divisions are granular enough + const rounder = isImperial ? 16 : 10 + + // we convert the used fabric height to the right units so we can round it + const inFabricUnits = height / (fabricUnit * unit) + // we multiply it by the rounder, round it up, then divide by the rounder again to get the rounded amount + const roundCount = Math.ceil(rounder * inFabricUnits) / rounder + // format as a fraction for imperial, a decimal for metric + const count = isImperial ? formatFraction128(roundCount, 'none') : round(roundCount, 1) + + return `${count}${isImperial ? 'yds' : 'm'}` +} + +export const CutLayoutSettings = ({ gist, patternProps, unsetGist, updateGist }) => { + const { t } = useTranslation(['workbench']) + + const fabricLength = useFabricLength(gist.units === 'imperial', patternProps.height) return (
@@ -15,13 +81,17 @@ export const CutLayoutSettings = (props) => { >
+
+ + {fabricLength} +
) } diff --git a/sites/shared/utils.mjs b/sites/shared/utils.mjs index e25b04409e7..adfc25a82f4 100644 --- a/sites/shared/utils.mjs +++ b/sites/shared/utils.mjs @@ -26,35 +26,52 @@ export const formatImperial = (neg, inch, numo = false, deno = false, format = ' } } +/** + * format a value to the nearest fraction with a denominator that is a power of 2 + * or a decimal if the value is between fractions + * NOTE: this method does not convert mm to inches. It will turn any given value directly into its equivalent fractional representation + * + * fraction: the value to process + * format: the type of formatting to apply. html, notags, or anything else which will only return numbers + */ +export const formatFraction128 = (fraction, format = 'html') => { + let negative = '' + let inches = '' + let rest = '' + if (fraction < 0) { + fraction = fraction * -1 + negative = '-' + } + if (Math.abs(fraction) < 1) rest = fraction + else { + inches = Math.floor(fraction) + rest = fraction - inches + } + let fraction128 = Math.round(rest * 128) + if (fraction128 == 0) return formatImperial(negative, inches, false, false, format) + + for (let i = 1; i < 7; i++) { + const numoFactor = Math.pow(2, 7 - i) + if (fraction128 % numoFactor === 0) + return formatImperial(negative, inches, fraction128 / numoFactor, Math.pow(2, i), format) + } + + return ( + negative + + Math.round(fraction * 100) / 100 + + (format === 'html' || format === 'notags' ? '"' : '') + ) +} + // Format a value in mm based on the user's units // Format can be html, notags, or anything else which will only return numbers export const formatMm = (val, units, format = 'html') => { val = roundMm(val) if (units === 'imperial') { if (val == 0) return formatImperial('', 0, false, false, format) - let negative = '' - let inches = '' - let rest = '' - let fraction = val / 25.4 - if (fraction < 0) { - fraction = fraction * -1 - negative = '-' - } - if (Math.abs(fraction) < 1) rest = fraction - else { - inches = Math.floor(fraction) - rest = fraction - inches - } - let fraction128 = Math.round(rest * 128) - if (fraction128 == 0) return formatImperial(negative, inches, false, false, format) - if (fraction128 % 64 == 0) return formatImperial(negative, inches, fraction128 / 64, 2, format) - if (fraction128 % 32 == 0) return formatImperial(negative, inches, fraction128 / 32, 4, format) - if (fraction128 % 16 == 0) return formatImperial(negative, inches, fraction128 / 16, 8, format) - if (fraction128 % 8 == 0) return formatImperial(negative, inches, fraction128 / 8, 16, format) - if (fraction128 % 4 == 0) return formatImperial(negative, inches, fraction128 / 4, 32, format) - if (fraction128 % 2 == 0) return formatImperial(negative, inches, fraction128 / 2, 64, format) - return negative + Math.round(fraction * 100) / 100 + '"' + let fraction = val / 25.4 + return formatFraction128(fraction, format) } else { if (format === 'html' || format === 'notags') return roundMm(val / 10) + 'cm' else return roundMm(val / 10) From 3bbea6d4c21f1059440dcf06867b9cb4539ec9d2 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Sun, 19 Feb 2023 16:24:46 +0200 Subject: [PATCH 03/67] mirror on fold macro --- packages/core/src/store.mjs | 2 + .../workbench/exporting/export-handler.mjs | 2 +- .../components/workbench/layout/cut/index.mjs | 8 ++- .../layout/cut/plugin-cut-layout.mjs | 52 +++++++++++++++++++ ...part-plugin.mjs => plugin-layout-part.mjs} | 0 .../workbench/layout/print/index.mjs | 2 +- 6 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs rename sites/shared/components/workbench/layout/{layout-part-plugin.mjs => plugin-layout-part.mjs} (100%) diff --git a/packages/core/src/store.mjs b/packages/core/src/store.mjs index db0bfbffeee..1ae9e5f0f29 100644 --- a/packages/core/src/store.mjs +++ b/packages/core/src/store.mjs @@ -35,9 +35,11 @@ export function Store(methods = []) { logs.info.push(...data) }, warning: function (...data) { + console.warn(...data) logs.warning.push(...data) }, error: function (...data) { + console.error(...data) logs.error.push(...data) }, } diff --git a/sites/shared/components/workbench/exporting/export-handler.mjs b/sites/shared/components/workbench/exporting/export-handler.mjs index 4c8174c8083..4a332c7dcf9 100644 --- a/sites/shared/components/workbench/exporting/export-handler.mjs +++ b/sites/shared/components/workbench/exporting/export-handler.mjs @@ -1,7 +1,7 @@ import Worker from 'web-worker' import fileSaver from 'file-saver' import { themePlugin } from '@freesewing/plugin-theme' -import { pagesPlugin } from '../layout/layout-part-plugin.mjs' +import { pagesPlugin } from '../layout/plugin-layout-part.mjs' import { capitalize } from 'shared/utils.mjs' export const exportTypes = { diff --git a/sites/shared/components/workbench/layout/cut/index.mjs b/sites/shared/components/workbench/layout/cut/index.mjs index 18a72ffbde2..eb5355d68ae 100644 --- a/sites/shared/components/workbench/layout/cut/index.mjs +++ b/sites/shared/components/workbench/layout/cut/index.mjs @@ -1,7 +1,9 @@ import { useTranslation } from 'next-i18next' import { CutLayoutSettings } from './settings.mjs' import { Draft } from '../draft/index.mjs' -import { fabricPlugin } from '../layout-part-plugin.mjs' +import { fabricPlugin } from '../plugin-layout-part.mjs' +import { cutLayoutPlugin } from './plugin-cut-layout.mjs' +import { pluginCutlist } from '@freesewing/plugin-cutlist' import { useEffect } from 'react' import { measurementAsMm } from 'shared/utils.mjs' @@ -19,11 +21,13 @@ export const CutLayout = (props) => { // add the pages plugin to the draft const layoutSettings = { - sheetWidth: gistSettings.sheetWidth || measurementAsMm(isImperial ? 54 : 50, props.gist.units), + sheetWidth: gistSettings.sheetWidth || measurementAsMm(isImperial ? 54 : 120, props.gist.units), sheetHeight: gistSettings.sheetHeight || measurementAsMm(isImperial ? 36 : 100, props.gist.units), } draft.use(fabricPlugin(layoutSettings)) + draft.use(pluginCutlist) + draft.use(cutLayoutPlugin) let patternProps try { diff --git a/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs b/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs new file mode 100644 index 00000000000..a1a3430b45a --- /dev/null +++ b/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs @@ -0,0 +1,52 @@ +const prefix = 'mirroredOnFold' + +export const cutLayoutPlugin = { + hooks: { + postPartDraft: (pattern) => { + const partCutlist = pattern.setStores[pattern.activeSet].get(['cutlist', pattern.activePart]) + if (!partCutlist) return + + const { macro } = pattern.parts[pattern.activeSet][pattern.activePart].shorthand() + if (partCutlist.cutOnFold) macro('mirrorOnFold', { fold: partCutlist.cutOnFold }) + }, + }, + macros: { + mirrorOnFold: ({ fold }, { paths, snippets, utils, macro, points }) => { + const mirrorPaths = [] + for (const p in paths) { + if (!paths[p].hidden) mirrorPaths.push(paths[p]) + } + + const mirrorPoints = [] + const snippetsByType = {} + for (var s in snippets) { + const snip = snippets[s] + if (['logo'].indexOf(snip.def) > -1) continue + + snippetsByType[snip.def] = snippetsByType[snip.def] || [] + + mirrorPoints.push(snip.anchor) + for (var pName in points) { + if (points[pName] === snip.anchor) { + snippetsByType[snip.def].push(prefix + utils.capitalize(pName)) + break + } + } + } + + macro('mirror', { + paths: mirrorPaths, + points: mirrorPoints, + mirror: fold, + prefix, + }) + + for (var def in snippetsByType) { + macro('sprinkle', { + snippet: def, + on: snippetsByType[def], + }) + } + }, + }, +} diff --git a/sites/shared/components/workbench/layout/layout-part-plugin.mjs b/sites/shared/components/workbench/layout/plugin-layout-part.mjs similarity index 100% rename from sites/shared/components/workbench/layout/layout-part-plugin.mjs rename to sites/shared/components/workbench/layout/plugin-layout-part.mjs diff --git a/sites/shared/components/workbench/layout/print/index.mjs b/sites/shared/components/workbench/layout/print/index.mjs index 3c5412649e7..9cd19287a14 100644 --- a/sites/shared/components/workbench/layout/print/index.mjs +++ b/sites/shared/components/workbench/layout/print/index.mjs @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'next-i18next' import { PrintLayoutSettings } from './settings.mjs' import { Draft } from '../draft/index.mjs' -import { pagesPlugin } from '../layout-part-plugin.mjs' +import { pagesPlugin } from '../plugin-layout-part.mjs' import { handleExport, defaultPdfSettings, From 6fe683be25b1be53ad2c93f9508bc2a7f227955a Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Mon, 20 Feb 2023 06:08:07 +0200 Subject: [PATCH 04/67] begin refactoring part resolution to handle one part at a time --- packages/core/src/pattern.mjs | 220 +++++++++------------- packages/core/src/store.mjs | 1 - packages/core/tests/pattern-init.test.mjs | 63 ++++++- 3 files changed, 149 insertions(+), 135 deletions(-) diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index 109bd62987c..aa796240e95 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -13,7 +13,7 @@ import { version } from '../data.mjs' import { __loadPatternDefaults } from './config.mjs' import cloneDeep from 'lodash.clonedeep' -const DISTANCE_DEBUG = false +const DISTANCE_DEBUG = true ////////////////////////////////////////////// // CONSTRUCTOR // @@ -74,11 +74,12 @@ export function Pattern(designConfig) { * @param {object} part - The part to add * @return {object} this - The Pattern instance */ -Pattern.prototype.addPart = function (part) { +Pattern.prototype.addPart = function (part, runtime = false) { if (typeof part?.draft === 'function') { if (part.name) { this.designConfig.parts.push(part) - this.__initialized = false + if (runtime) { + } else this.__initialized = false } else this.store.log.error(`Part must have a name`) } else this.store.log.error(`Part must have a draft() method`) @@ -370,6 +371,7 @@ Pattern.prototype.use = function (plugin, data) { */ Pattern.prototype.__addDependency = function (name, part, dep) { this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name]) + // #FIXME What's supposed to happen here? if (typeof this.__designParts[dep.name] === 'undefined') { this.config = this.__addPartConfig(this.__designParts[dep.name]) } @@ -417,12 +419,6 @@ Pattern.prototype.__addPartMeasurements = function (part, list = false) { } } } - if (part.from) this.__addPartMeasurements(part.from, list) - if (part.after) { - if (Array.isArray(part.after)) { - for (const dep of part.after) this.__addPartMeasurements(dep, list) - } else this.__addPartMeasurements(part.after, list) - } // Weed out duplicates this.config.measurements = [...new Set(list)] @@ -452,12 +448,6 @@ Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) } } } - if (part.from) this.__addPartOptionalMeasurements(part.from, list) - if (part.after) { - if (Array.isArray(part.after)) { - for (const dep of part.after) this.__addPartOptionalMeasurements(dep, list) - } else this.__addPartOptionalMeasurements(part.after, list) - } // Weed out duplicates if (list.length > 0) this.config.optionalMeasurements = [...new Set(list)] @@ -475,37 +465,27 @@ Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) Pattern.prototype.__addPartOptions = function (part) { if (!this.config.options) this.config.options = {} if (part.options) { + const partDistance = this.__mutated.partDistance?.[part.name] || 0 for (const optionName in part.options) { - if (!this.__mutated.optionDistance[optionName]) { - this.__mutated.optionDistance[optionName] = this.__mutated.partDistance?.[part.name] || 0 + const optionDistance = this.__mutated.optionDistance[optionName] + if (!optionDistance) { + this.__mutated.optionDistance[optionName] = partDistance // Keep design parts immutable in the pattern or risk subtle bugs this.config.options[optionName] = Object.freeze(part.options[optionName]) this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``) } else { if (DISTANCE_DEBUG) this.store.log.debug( - 'optionDistance for ' + - optionName + - ' is ' + - this.__mutated.optionDistance[optionName] + - ', and partDistance for ' + - part.name + - ' is ' + - this.__mutated.partDistance[part.name] + `optionDistance for ${optionName} is ${optionDistance} and partDistance for ${part.name} is ${partDistance}` ) - if (this.__mutated.optionDistance[optionName] > this.__mutated.partDistance[part.name]) { + if (optionDistance > partDistance) { this.config.options[optionName] = part.options[optionName] + this.__mutated.optionDistance[optionName] = partDistance this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``) } } } } - if (part.from) this.__addPartOptions(part.from) - if (part.after) { - if (Array.isArray(part.after)) { - for (const dep of part.after) this.__addPartOptions(dep) - } else this.__addPartOptions(part.after) - } return this } @@ -1272,6 +1252,76 @@ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDepende return this } +Pattern.prototype.__resolvePartMutation = function (part, dependency, depType) { + const current_part_distance = this.__mutated.partDistance[part.name] + const proposed_dependent_part_distance = current_part_distance + 1 + + this.__designParts[dependency.name] = Object.freeze(dependency) + switch (depType) { + case 'from': + this.__setFromHide(part, part.name, dependency.name) + this.__inject[part.name] = dependency.name + break + case 'after': + this.__setAfterHide(part, part.name, dependency.name) + this.__addDependency(part.name, part, dependency) + } + + if ( + typeof this.__mutated.partDistance[dependency.name] === 'undefined' || + this.__mutated.partDistance[dependency.name] < proposed_dependent_part_distance + ) { + this.__mutated.partDistance[dependency.name] = proposed_dependent_part_distance + if (DISTANCE_DEBUG) + this.store.log.debug( + `"${depType}:" partDistance for ${dependency.name} is ${ + this.__mutated.partDistance[dependency.name] + }` + ) + } +} + +Pattern.prototype.__resolvePart = function (part, distance = 0) { + if (distance === 0) { + this.__designParts[part.name] = Object.freeze(part) + } + let count = Object.keys(this.__designParts).length + distance++ + if (typeof this.__mutated.partDistance[part.name] === 'undefined') { + this.__mutated.partDistance[part.name] = distance + + if (DISTANCE_DEBUG) + this.store.log.debug( + `Base partDistance for ${part.name} is ${this.__mutated.partDistance[part.name]}` + ) + } + + // Hide when hideAll is set + if (part.hideAll) { + this.__mutated.partHide[part.name] = true + } + + // Resolve part mutations. first from then after + ;['from', 'after'].forEach((d) => { + if (part[d]) { + if (DISTANCE_DEBUG) this.store.log.debug(`Processing ${part.name} "${d}:"`) + + const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] + + depsOfType.forEach((dot) => { + this.__resolvePartMutation(part, dot, d) + const newCount = Object.keys(this.__designParts).length + if (count < newCount) { + this.__resolvePart(dot, distance) + count = newCount + } + }) + } + }) + + // add the part's config + this.__addPartConfig(part) +} /** * Resolves parts and their dependencies * @@ -1281,113 +1331,21 @@ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDepende * @return {Pattern} this - The Pattern instance */ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { - if (count === 0) { - for (const part of this.designConfig.parts) { - // Keep design parts immutable in the pattern or risk subtle bugs - this.__designParts[part.name] = Object.freeze(part) - } - } - distance++ - if (DISTANCE_DEBUG) this.store.log.debug('Distance incremented to ' + distance) for (const part of this.designConfig.parts) { - if (typeof this.__mutated.partDistance[part.name] === 'undefined') { - this.__mutated.partDistance[part.name] = distance - if (DISTANCE_DEBUG) - this.store.log.debug( - 'Base partDistance for ' + part.name + ' is ' + this.__mutated.partDistance[part.name] - ) - } - } - for (const [name, part] of Object.entries(this.__designParts)) { - const current_part_distance = this.__mutated.partDistance[part.name] - const proposed_dependent_part_distance = current_part_distance + 1 - // Hide when hideAll is set - if (part.hideAll) this.__mutated.partHide[part.name] = true - // Inject (from) - if (part.from) { - if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "from:"') - this.__setFromHide(part, name, part.from.name) - this.__designParts[part.from.name] = part.from - this.__inject[name] = part.from.name - if ( - typeof this.__mutated.partDistance[part.from.name] === 'undefined' || - this.__mutated.partDistance[part.from.name] < proposed_dependent_part_distance - ) { - this.__mutated.partDistance[part.from.name] = proposed_dependent_part_distance - if (DISTANCE_DEBUG) - this.store.log.debug( - '"from:" partDistance for ' + - part.from.name + - ' is ' + - this.__mutated.partDistance[part.from.name] - ) - } - } - // Simple dependency (after) - if (part.after) { - if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "after:"') - if (Array.isArray(part.after)) { - for (const dep of part.after) { - this.__setAfterHide(part, name, dep.name) - this.__designParts[dep.name] = dep - this.__addDependency(name, part, dep) - if ( - typeof this.__mutated.partDistance[dep.name] === 'undefined' || - this.__mutated.partDistance[dep.name] < proposed_dependent_part_distance - ) { - this.__mutated.partDistance[dep.name] = proposed_dependent_part_distance - if (DISTANCE_DEBUG) - this.store.log.debug( - '"after:" partDistance for ' + - dep.name + - ' is ' + - this.__mutated.partDistance[dep.name] - ) - } - } - } else { - this.__setAfterHide(part, name, part.after.name) - this.__designParts[part.after.name] = part.after - this.__addDependency(name, part, part.after) - if ( - typeof this.__mutated.partDistance[part.after.name] === 'undefined' || - this.__mutated.partDistance[part.after.name] < proposed_dependent_part_distance - ) { - this.__mutated.partDistance[part.after.name] = proposed_dependent_part_distance - if (DISTANCE_DEBUG) - this.store.log.debug( - '"after:" partDistance for ' + - part.after.name + - ' is ' + - this.__mutated.partDistance[part.after.name] - ) - } - } - } - } - // Did we discover any new dependencies? - const len = Object.keys(this.__designParts).length - // If so, resolve recursively - if (len > count) { - if (DISTANCE_DEBUG) this.store.log.debug('Recursing...') - return this.__resolveParts(len, distance) + this.__resolvePart(part, distance) } + // Print final part distances. for (const part of this.designConfig.parts) { - let qualifier = '' - if (DISTANCE_DEBUG) qualifier = 'final ' + let qualifier = DISTANCE_DEBUG ? 'final' : '' this.store.log.debug( - '⚪️ `' + - part.name + - '` ' + - qualifier + - 'options priority is __' + - this.__mutated.partDistance[part.name] + - '__' + `⚪️ ${part.name} ${qualifier} options priority is __${ + this.__mutated.partDistance[part.name] + }__` ) } - for (const part of Object.values(this.__designParts)) this.__addPartConfig(part) + // for (const part of Object.values(this.__designParts)) this.__addPartConfig(part) return this } diff --git a/packages/core/src/store.mjs b/packages/core/src/store.mjs index 1ae9e5f0f29..c8f5fad5fbd 100644 --- a/packages/core/src/store.mjs +++ b/packages/core/src/store.mjs @@ -35,7 +35,6 @@ export function Store(methods = []) { logs.info.push(...data) }, warning: function (...data) { - console.warn(...data) logs.warning.push(...data) }, error: function (...data) { diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index d3d5bcfc56f..bebc5f09b97 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -111,7 +111,7 @@ describe('Pattern', () => { parts: [partC], }) const pattern = new Pattern() - pattern.draft() + pattern.__init() it('Pattern.__init() should resolve all measurements', () => { expect( @@ -153,8 +153,8 @@ describe('Pattern', () => { }) it('Pattern.__init() should set config data in the store', () => { - expect(pattern.setStores[0].get('data.name')).to.equal('test') - expect(pattern.setStores[0].get('data.version')).to.equal('1.2.3') + expect(pattern.store.get('data.name')).to.equal('test') + expect(pattern.store.get('data.version')).to.equal('1.2.3') }) it('Pattern.__init() should resolve dependencies', () => { @@ -181,6 +181,63 @@ describe('Pattern', () => { expect(pattern.config.draftOrder[2]).to.equal('test.partC') }) + it('Pattern.__init() should overwrite options from dependencies', () => { + const partD = { + name: 'test.partD', + from: partB, + options: { + optB: { deg: 25, min: 15, max: 45 }, + }, + draft: ({ part }) => part, + } + + const Pattern = new Design({ + data: { + name: 'test', + version: '1.2.3', + }, + parts: [partD], + }) + const pattern = new Pattern() + pattern.__init() + for (const [key, value] of Object.entries(partD.options.optB)) { + expect(pattern.config.options.optB[key]).to.equal(value) + } + }) + + it('Pattern.__init() should overwrite options from complex dependencies', () => { + const partD = { + name: 'test.partD', + from: partB, + options: { + optB: { deg: 25, min: 15, max: 45 }, + }, + draft: ({ part }) => part, + } + + const partE = { + name: 'test.partE', + from: partD, + options: { + optB: { deg: 10, min: 15, max: 50 }, + }, + draft: ({ part }) => part, + } + + const Pattern = new Design({ + data: { + name: 'test', + version: '1.2.3', + }, + parts: [partC, partE], + }) + const pattern = new Pattern() + pattern.__init() + for (const [key, value] of Object.entries(partE.options.optB)) { + expect(pattern.config.options.optB[key]).to.equal(value) + } + }) + // I am aware this does too much for one unit test, but this is to simplify TDD // we can split it up later it('Pattern.__init() should resolve nested injections', () => { From 07a1a3c0f1b78986481d7e56d96a0cdf1b595881 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Mon, 20 Feb 2023 23:59:28 +0200 Subject: [PATCH 05/67] all part resolution business happens on a per-part basis --- packages/core/src/pattern.mjs | 244 ++++++++-------------- packages/core/tests/pattern-init.test.mjs | 8 +- 2 files changed, 91 insertions(+), 161 deletions(-) diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index aa796240e95..e5c7ff9e25a 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -13,8 +13,7 @@ import { version } from '../data.mjs' import { __loadPatternDefaults } from './config.mjs' import cloneDeep from 'lodash.clonedeep' -const DISTANCE_DEBUG = true - +const DISTANCE_DEBUG = false ////////////////////////////////////////////// // CONSTRUCTOR // ////////////////////////////////////////////// @@ -74,12 +73,11 @@ export function Pattern(designConfig) { * @param {object} part - The part to add * @return {object} this - The Pattern instance */ -Pattern.prototype.addPart = function (part, runtime = false) { +Pattern.prototype.addPart = function (part) { if (typeof part?.draft === 'function') { if (part.name) { this.designConfig.parts.push(part) - if (runtime) { - } else this.__initialized = false + this.__initialized = false } else this.store.log.error(`Part must have a name`) } else this.store.log.error(`Part must have a draft() method`) @@ -369,14 +367,12 @@ Pattern.prototype.use = function (plugin, data) { * @param {object} dep - The dependency configuration * @return {object} this - The Pattern instance */ -Pattern.prototype.__addDependency = function (name, part, dep) { - this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name]) - // #FIXME What's supposed to happen here? - if (typeof this.__designParts[dep.name] === 'undefined') { - this.config = this.__addPartConfig(this.__designParts[dep.name]) - } - - return this +Pattern.prototype.__addDependency = function (dependencyList, part, dep) { + this[dependencyList][part.name] = this[dependencyList][part.name] || [] + if (dependencyList == '__resolvedDependencies' && DISTANCE_DEBUG) + this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`) + if (this[dependencyList][part.name].indexOf(dep.name) === -1) + this[dependencyList][part.name].push(dep.name) } /** @@ -476,7 +472,7 @@ Pattern.prototype.__addPartOptions = function (part) { } else { if (DISTANCE_DEBUG) this.store.log.debug( - `optionDistance for ${optionName} is ${optionDistance} and partDistance for ${part.name} is ${partDistance}` + `optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__` ) if (optionDistance > partDistance) { this.config.options[optionName] = part.options[optionName] @@ -552,6 +548,11 @@ Pattern.prototype.__addPartPlugins = function (part) { `Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.` ) } + // swap from a conditional if needed + else if (plugins[name].condition) { + plugins[name] = plugin + this.store.log.info(`Plugin \`${name}\` was explicitly added. Changing from conditional.`) + } } } @@ -741,21 +742,11 @@ Pattern.prototype.__isPartHidden = function (partName) { Pattern.prototype.__isStackHidden = function (stackName) { if (!this.stacks[stackName]) return true const parts = this.stacks[stackName].getPartNames() - if (Array.isArray(this.settings[this.activeStack || 0].only)) { - for (const partName of parts) { - if (this.settings[this.activeStack || 0].only.includes(partName)) return false - } - return true - } for (const partName of parts) { - if (this.__designParts?.[partName]?.hide) return true - if (this.__designParts?.[partName]?.hideAll) return true - if (this.__mutated.partHide?.[partName]) return true - if (this.__mutated.partHideAll?.[partName]) return true - if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true + if (!this.__isPartHidden(partName)) return false } - return false + return true } /** @@ -1189,35 +1180,6 @@ Pattern.prototype.__pack = function () { return this } -/** - * Recursively solves part dependencies for a part - * - * @private - * @param {object} seen - Object to keep track of seen dependencies - * @param {string} part - Name of the part - * @param {object} graph - Dependency graph, used to call itself recursively - * @param {array} deps - List of dependencies - * @return {Array} deps - The list of dependencies - */ -Pattern.prototype.__resolveDependency = function ( - seen, - part, - graph = this.dependencies, - deps = [] -) { - if (typeof seen[part] === 'undefined') seen[part] = true - if (typeof graph[part] === 'string') graph[part] = [graph[part]] - if (Array.isArray(graph[part])) { - if (graph[part].length === 0) return [] - else { - if (deps.indexOf(graph[part]) === -1) deps.push(...graph[part]) - for (let apart of graph[part]) deps.concat(this.__resolveDependency(seen, apart, graph, deps)) - } - } - - return deps -} - /** * Resolves the draft order based on the configuation * @@ -1226,37 +1188,23 @@ Pattern.prototype.__resolveDependency = function ( * @return {Pattern} this - The Pattern instance */ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDependencies) { - let sorted = [] - let visited = {} - Object.keys(graph).forEach(function visit(name, ancestors) { - if (!Array.isArray(ancestors)) ancestors = [] - ancestors.push(name) - visited[name] = true - if (typeof graph[name] !== 'undefined') { - graph[name].forEach(function (dep) { - if (visited[dep]) return - visit(dep, ancestors.slice(0)) - }) - } - if (sorted.indexOf(name) < 0) sorted.push(name) - }) - - // Don't forget about parts without dependencies - for (const part in this.__designParts) { - if (sorted.indexOf(part) === -1) sorted.push(part) - } - + const sorted = Object.keys(this.__designParts).sort( + (p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1] + ) this.__draftOrder = sorted this.config.draftOrder = sorted return this } -Pattern.prototype.__resolvePartMutation = function (part, dependency, depType) { - const current_part_distance = this.__mutated.partDistance[part.name] - const proposed_dependent_part_distance = current_part_distance + 1 +Pattern.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) { + const part = depChain[0] this.__designParts[dependency.name] = Object.freeze(dependency) + this.__addDependency('__dependencies', part, dependency) + + depChain.forEach((c) => this.__addDependency('__resolvedDependencies', c, dependency)) + switch (depType) { case 'from': this.__setFromHide(part, part.name, dependency.name) @@ -1264,35 +1212,72 @@ Pattern.prototype.__resolvePartMutation = function (part, dependency, depType) { break case 'after': this.__setAfterHide(part, part.name, dependency.name) - this.__addDependency(part.name, part, dependency) - } - - if ( - typeof this.__mutated.partDistance[dependency.name] === 'undefined' || - this.__mutated.partDistance[dependency.name] < proposed_dependent_part_distance - ) { - this.__mutated.partDistance[dependency.name] = proposed_dependent_part_distance - if (DISTANCE_DEBUG) - this.store.log.debug( - `"${depType}:" partDistance for ${dependency.name} is ${ - this.__mutated.partDistance[dependency.name] - }` - ) } } -Pattern.prototype.__resolvePart = function (part, distance = 0) { +Pattern.prototype.__resolveMutatedPartDistance = function (partName) { + const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1 + let didChange = false + if (!this.__dependencies[partName]) return false + this.__dependencies[partName].forEach((dependency) => { + if ( + typeof this.__mutated.partDistance[dependency] === 'undefined' || + this.__mutated.partDistance[dependency] < proposed_dependent_part_distance + ) { + didChange = true + this.__mutated.partDistance[dependency] = proposed_dependent_part_distance + this.__resolveMutatedPartDistance(dependency) + } + if (DISTANCE_DEBUG) + this.store.log.debug( + `"${depType}:" partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` + ) + }) + + return didChange +} + +const depTypes = ['from', 'after'] +Pattern.prototype.__resolvePartDependencies = function (depChain, distance) { + // Resolve part Dependencies. first from then after + const part = depChain[0] + this.__resolvedDependencies[part.name] = this.__resolvedDependencies[part.name] || [] + + depTypes.forEach((d) => { + if (part[d]) { + if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`) + + const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] + + depsOfType.forEach((dot) => { + let count = Object.keys(this.__designParts).length + // if any changes resulted from resolving this part mutation + this.__resolvePartDependencyChain(depChain, dot, d) + // if a new part was added, resolve the part + const newCount = Object.keys(this.__designParts).length + if (count < newCount) { + this.__resolvePart([dot, ...depChain], distance) + count = newCount + } + }) + } + }) + + this.__resolveMutatedPartDistance(part.name) +} + +Pattern.prototype.__resolvePart = function (depChain, distance = 0) { + const part = depChain[0] if (distance === 0) { this.__designParts[part.name] = Object.freeze(part) } - let count = Object.keys(this.__designParts).length distance++ if (typeof this.__mutated.partDistance[part.name] === 'undefined') { this.__mutated.partDistance[part.name] = distance if (DISTANCE_DEBUG) this.store.log.debug( - `Base partDistance for ${part.name} is ${this.__mutated.partDistance[part.name]}` + `Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__` ) } @@ -1301,23 +1286,7 @@ Pattern.prototype.__resolvePart = function (part, distance = 0) { this.__mutated.partHide[part.name] = true } - // Resolve part mutations. first from then after - ;['from', 'after'].forEach((d) => { - if (part[d]) { - if (DISTANCE_DEBUG) this.store.log.debug(`Processing ${part.name} "${d}:"`) - - const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] - - depsOfType.forEach((dot) => { - this.__resolvePartMutation(part, dot, d) - const newCount = Object.keys(this.__designParts).length - if (count < newCount) { - this.__resolvePart(dot, distance) - count = newCount - } - }) - } - }) + this.__resolvePartDependencies(depChain, distance) // add the part's config this.__addPartConfig(part) @@ -1332,21 +1301,19 @@ Pattern.prototype.__resolvePart = function (part, distance = 0) { */ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { for (const part of this.designConfig.parts) { - this.__resolvePart(part, distance) + this.__resolvePart([part], distance) } // Print final part distances. for (const part of this.designConfig.parts) { let qualifier = DISTANCE_DEBUG ? 'final' : '' this.store.log.debug( - `⚪️ ${part.name} ${qualifier} options priority is __${ + `⚪️ \`${part.name}\` ${qualifier} options priority is __${ this.__mutated.partDistance[part.name] }__` ) } - // for (const part of Object.values(this.__designParts)) this.__addPartConfig(part) - return this } @@ -1354,35 +1321,10 @@ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { * Resolves parts depdendencies into a flat array * * @private - * @param {object} graph - The graph is used to call itsels recursively * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.__resolveDependencies = function (graph = false) { - if (!graph) graph = this.__dependencies - for (const i in this.__inject) { - const dependency = this.__inject[i] - if (typeof this.__dependencies[i] === 'undefined') this.__dependencies[i] = dependency - else if (this.__dependencies[i] !== dependency) { - if (typeof this.__dependencies[i] === 'string') { - this.__dependencies[i] = [this.__dependencies[i], dependency] - } else if (Array.isArray(this.__dependencies[i])) { - if (this.__dependencies[i].indexOf(dependency) === -1) - this.__dependencies[i].push(dependency) - } else { - this.store.log.error('Part dependencies should be a string or an array of strings') - throw new Error('Part dependencies should be a string or an array of strings') - } - } - } - - let resolved = {} - let seen = {} - for (let part in graph) resolved[part] = this.__resolveDependency(seen, part, graph) - for (let part in seen) if (typeof resolved[part] === 'undefined') resolved[part] = [] - - this.__resolvedDependencies = resolved - this.config.resolvedDependencies = resolved - +Pattern.prototype.__resolveDependencies = function () { + this.config.resolvedDependencies = this.__resolvedDependencies return this } @@ -1563,19 +1505,7 @@ Pattern.prototype.__wants = function (partName, set = 0) { */ function mergeDependencies(dep = [], current = []) { // Current dependencies - const list = [] - if (Array.isArray(current)) list.push(...current) - else if (typeof current === 'string') list.push(current) + const list = [].concat(current, dep) - if (Array.isArray(dep)) list.push(...dep) - else if (typeof dep === 'string') list.push(dep) - - // Dependencies should be parts names (string) not the object - const deps = [] - for (const part of [...new Set(list)]) { - if (typeof part === 'object') deps.push(part.name) - else deps.push(part) - } - - return deps + return [...new Set(list)] } diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index bebc5f09b97..8f5c76621d7 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -324,10 +324,10 @@ describe('Pattern', () => { expect(pattern.config.options.optionR.list[1]).to.equal('green') expect(pattern.config.options.optionR.list[2]).to.equal('blue') // Dependencies - expect(pattern.__dependencies.partB[0]).to.equal('partA') - expect(pattern.__dependencies.partC[0]).to.equal('partB') - expect(pattern.__dependencies.partR[0]).to.equal('partC') - expect(pattern.__dependencies.partR[1]).to.equal('partA') + expect(pattern.__dependencies.partB).to.include('partA') + expect(pattern.__dependencies.partC).to.include('partB') + expect(pattern.__dependencies.partR).to.include('partC') + expect(pattern.__dependencies.partR).to.include('partA') // Inject expect(pattern.__inject.partB).to.equal('partA') expect(pattern.__inject.partC).to.equal('partB') From d4675d864833e82c2667a46f1475072eb8c6b75e Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Tue, 21 Feb 2023 16:17:57 +0200 Subject: [PATCH 06/67] add tests for runtime part adding --- packages/core/src/pattern.mjs | 7 +- .../core/tests/pattern-runtime-parts.test.mjs | 101 ++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/core/tests/pattern-runtime-parts.test.mjs diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index e5c7ff9e25a..f9abd74be97 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -73,11 +73,14 @@ export function Pattern(designConfig) { * @param {object} part - The part to add * @return {object} this - The Pattern instance */ -Pattern.prototype.addPart = function (part) { +Pattern.prototype.addPart = function (part, resolveImmediately = false) { if (typeof part?.draft === 'function') { if (part.name) { this.designConfig.parts.push(part) - this.__initialized = false + if (resolveImmediately) { + this.store.log.debug(`Perfoming runtime resolution of new part ${part.name}`) + this.__resolvePart([part]) + } else this.__initialized = false } else this.store.log.error(`Part must have a name`) } else this.store.log.error(`Part must have a draft() method`) diff --git a/packages/core/tests/pattern-runtime-parts.test.mjs b/packages/core/tests/pattern-runtime-parts.test.mjs new file mode 100644 index 00000000000..a3a640e6721 --- /dev/null +++ b/packages/core/tests/pattern-runtime-parts.test.mjs @@ -0,0 +1,101 @@ +import chai from 'chai' +import { Design } from '../src/index.mjs' + +const expect = chai.expect + +describe('Pattern', () => { + describe('.addPart()', () => { + const part1 = { + name: 'test', + draft: ({ part }) => part, + } + + const part2 = { + name: 'test2', + from: part1, + draft: ({ part }) => part, + } + + const part3 = { + name: 'test3', + after: part2, + draft: ({ part }) => part, + } + + describe('with resolveImmediately: true', () => { + it('Should add the part to the internal part object', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + pattern.addPart(part2, true) + expect(pattern.__designParts.test2).to.equal(part2) + }) + + it('Should resolve injected dependencies for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + pattern.addPart(part2, true) + expect(pattern.__inject.test2).to.equal('test') + }) + + it('Should resolve all dependencies for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + pattern.addPart(part3, true) + expect(pattern.config.resolvedDependencies.test3).to.have.members(['test', 'test2']) + expect(pattern.__designParts.test2).to.equal(part2) + }) + + it('Should add a the measurements for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const part2 = { + name: 'test2', + measurements: ['neck'], + draft: ({ part }) => part, + } + + pattern.addPart(part2, true) + expect(pattern.config.measurements).to.include('neck') + }) + + it('Should add the plugins for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const plugin = { name: 'testPlugin' } + const part2 = { + name: 'test2', + plugins: [plugin], + draft: ({ part }) => part, + } + + pattern.addPart(part2, true) + expect(pattern.config.plugins.testPlugin).to.equal(plugin) + }) + + it('Should add the options for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const opt1 = { pct: 10, min: 0, max: 50 } + const part2 = { + name: 'test2', + options: { + opt1, + }, + draft: ({ part }) => part, + } + + pattern.addPart(part2, true) + expect(pattern.config.options.opt1).to.equal(opt1) + }) + }) + }) +}) From e2ca4b38c43e53d7f940971bcca950c98cca1ad4 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Tue, 21 Feb 2023 22:33:57 +0200 Subject: [PATCH 07/67] move all pattern config resolution to separate class --- packages/core/src/pattern.mjs | 502 ++---------------- packages/core/src/patternConfig.mjs | 419 +++++++++++++++ packages/core/tests/pattern-init.test.mjs | 38 +- .../core/tests/pattern-runtime-parts.test.mjs | 71 ++- packages/core/tests/snap.test.mjs | 3 + 5 files changed, 534 insertions(+), 499 deletions(-) create mode 100644 packages/core/src/patternConfig.mjs diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index f9abd74be97..d593c360304 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -11,9 +11,9 @@ import { Store } from './store.mjs' import { Hooks } from './hooks.mjs' import { version } from '../data.mjs' import { __loadPatternDefaults } from './config.mjs' +import { PatternConfig, getPluginName } from './patternConfig.mjs' import cloneDeep from 'lodash.clonedeep' -const DISTANCE_DEBUG = false ////////////////////////////////////////////// // CONSTRUCTOR // ////////////////////////////////////////////// @@ -25,7 +25,7 @@ const DISTANCE_DEBUG = false * @param {object} config - The Design config * @return {object} this - The Pattern instance */ -export function Pattern(designConfig) { +export function Pattern(designConfig = {}) { // Non-enumerable properties __addNonEnumProp(this, 'plugins', {}) __addNonEnumProp(this, 'width', 0) @@ -39,19 +39,9 @@ export function Pattern(designConfig) { __addNonEnumProp(this, 'Attributes', Attributes) __addNonEnumProp(this, 'macros', {}) __addNonEnumProp(this, '__initialized', false) - __addNonEnumProp(this, '__designParts', {}) - __addNonEnumProp(this, '__inject', {}) - __addNonEnumProp(this, '__dependencies', {}) - __addNonEnumProp(this, '__resolvedDependencies', {}) - __addNonEnumProp(this, '__resolvedParts', []) + __addNonEnumProp(this, 'config.parts', {}) + __addNonEnumProp(this, 'config.resolvedDependencies', {}) __addNonEnumProp(this, '__storeMethods', new Set()) - __addNonEnumProp(this, '__mutated', { - optionDistance: {}, - partDistance: {}, - partHide: {}, - partHideAll: {}, - }) - __addNonEnumProp(this, '__draftOrder', []) __addNonEnumProp(this, '__hide', {}) // Enumerable properties @@ -59,6 +49,7 @@ export function Pattern(designConfig) { this.config = {} // Will hold the resolved pattern after calling __init() this.store = new Store() // Pattern-wide store this.setStores = [] // Per-set stores + __addNonEnumProp(this, '__configResolver', new PatternConfig(this)) // handles config resolution during __init() as well as runtime part adding return this } @@ -74,16 +65,11 @@ export function Pattern(designConfig) { * @return {object} this - The Pattern instance */ Pattern.prototype.addPart = function (part, resolveImmediately = false) { - if (typeof part?.draft === 'function') { - if (part.name) { - this.designConfig.parts.push(part) - if (resolveImmediately) { - this.store.log.debug(`Perfoming runtime resolution of new part ${part.name}`) - this.__resolvePart([part]) - } else this.__initialized = false - } else this.store.log.error(`Part must have a name`) - } else this.store.log.error(`Part must have a draft() method`) - + if (this.__configResolver.validatePart(part) && this.designConfig.parts.indexOf(part) === -1) { + this.designConfig.parts.push(part) + if (resolveImmediately) this.__configResolver.addPart(part) + else this.__initialized = false + } return this } @@ -132,15 +118,15 @@ Pattern.prototype.createPartForSet = function (partName, set = 0) { this.parts[set][partName] = this.__createPartWithContext(partName, set) // Handle inject/inheritance - if (typeof this.__inject[partName] === 'string') { + if (typeof this.config.inject[partName] === 'string') { this.setStores[set].log.debug( - `Creating part \`${partName}\` from part \`${this.__inject[partName]}\`` + `Creating part \`${partName}\` from part \`${this.config.inject[partName]}\`` ) try { - this.parts[set][partName].__inject(this.parts[set][this.__inject[partName]]) + this.parts[set][partName].__inject(this.parts[set][this.config.inject[partName]]) } catch (err) { this.setStores[set].log.error([ - `Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``, + `Could not inject part \`${this.config.inject[partName]}\` into part \`${partName}\``, err, ]) } @@ -161,11 +147,11 @@ Pattern.prototype.createPartForSet = function (partName, set = 0) { } Pattern.prototype.draftPartForSet = function (partName, set) { - if (typeof this.__designParts?.[partName]?.draft === 'function') { + if (typeof this.config.parts?.[partName]?.draft === 'function') { this.activePart = partName try { this.__runHooks('prePartDraft') - const result = this.__designParts[partName].draft(this.parts[set][partName].shorthand()) + const result = this.config.parts[partName].draft(this.parts[set][partName].shorthand()) this.__runHooks('postPartDraft') if (typeof result === 'undefined') { this.setStores[set].log.error( @@ -361,209 +347,6 @@ Pattern.prototype.use = function (plugin, data) { // PRIVATE METHODS // ////////////////////////////////////////////// -/** - * Adds a part as a simple dependency - * - * @private - * @param {string} name - The name of the dependency - * @param {object} part - The part configuration - * @param {object} dep - The dependency configuration - * @return {object} this - The Pattern instance - */ -Pattern.prototype.__addDependency = function (dependencyList, part, dep) { - this[dependencyList][part.name] = this[dependencyList][part.name] || [] - if (dependencyList == '__resolvedDependencies' && DISTANCE_DEBUG) - this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`) - if (this[dependencyList][part.name].indexOf(dep.name) === -1) - this[dependencyList][part.name].push(dep.name) -} - -/** - * Resolves/Adds a part's design configuration to the pattern config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @param {onject} config - The global config - * @param {Store} store - The store, used for logging - * @return {object} config - The mutated global config - */ -Pattern.prototype.__addPartConfig = function (part) { - if (this.__resolvedParts.includes(part.name)) return this - - // Add parts, using set to keep them unique in the array - this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)] - - return this.__addPartOptions(part) - .__addPartMeasurements(part) - .__addPartOptionalMeasurements(part) - .__addPartPlugins(part) -} - -/** - * Resolves/Adds a part's configured measurements to the global config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @param {array} list - The list of resolved measurements - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__addPartMeasurements = function (part, list = false) { - if (!this.config.measurements) this.config.measurements = [] - if (!list) list = this.config.measurements - if (part.measurements) { - for (const m of part.measurements) { - if (list.indexOf(m) === -1) { - list.push(m) - this.store.log.debug(`🟠 __${m}__ measurement is required in \`${part.name}\``) - } - } - } - - // Weed out duplicates - this.config.measurements = [...new Set(list)] - - return this -} - -/** - * Resolves/Adds a part's configured optional measurements to the global config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @param {array} list - The list of resolved optional measurements - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) { - if (!this.config.optionalMeasurements) this.config.optionalMeasurements = [] - if (!list) list = this.config.optionalMeasurements - if (part.optionalMeasurements) { - for (const m of part.optionalMeasurements) { - // Don't add it's a required measurement for another part - if (this.config.measurements.indexOf(m) === -1) { - if (list.indexOf(m) === -1) { - list.push(m) - this.store.log.debug(`🟡 __${m}__ measurement is optional in \`${part.name}\``) - } - } - } - } - - // Weed out duplicates - if (list.length > 0) this.config.optionalMeasurements = [...new Set(list)] - - return this -} - -/** - * Resolves/Adds a part's configured options to the global config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__addPartOptions = function (part) { - if (!this.config.options) this.config.options = {} - if (part.options) { - const partDistance = this.__mutated.partDistance?.[part.name] || 0 - for (const optionName in part.options) { - const optionDistance = this.__mutated.optionDistance[optionName] - if (!optionDistance) { - this.__mutated.optionDistance[optionName] = partDistance - // Keep design parts immutable in the pattern or risk subtle bugs - this.config.options[optionName] = Object.freeze(part.options[optionName]) - this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``) - } else { - if (DISTANCE_DEBUG) - this.store.log.debug( - `optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__` - ) - if (optionDistance > partDistance) { - this.config.options[optionName] = part.options[optionName] - this.__mutated.optionDistance[optionName] = partDistance - this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``) - } - } - } - } - - return this -} - -function getPluginName(plugin) { - if (Array.isArray(plugin)) { - if (plugin[0].name) return plugin[0].name - if (plugin[0].plugin.name) return plugin[0].plugin.name - } else { - if (plugin.name) return plugin.name - if (plugin.plugin?.name) return plugin.plugin.name - } - - return false -} - -/** - * Resolves/Adds a part's configured plugins to the global config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__addPartPlugins = function (part) { - if (!part.plugins) return this - if (!this.config.plugins) this.config.plugins = {} - const plugins = { ...this.config.plugins } - // Side-step immutability of the part object to ensure plugins is an array - let partPlugins = part.plugins - if (!Array.isArray(partPlugins)) partPlugins = [partPlugins] - // Go through list of part plugins - for (let plugin of partPlugins) { - const name = getPluginName(plugin) - this.store.log.debug( - plugin.plugin - ? `🔌 Resolved __${name}__ conditional plugin in \`${part.name}\`` - : `🔌 Resolved __${name}__ plugin in \`${part.name}\`` - ) - // Handle [plugin, data] scenario - if (Array.isArray(plugin)) { - const pluginObj = { ...plugin[0], data: plugin[1] } - plugin = pluginObj - } - if (!plugins[name]) { - // New plugin, so we load it - plugins[name] = plugin - this.store.log.info( - plugin.condition - ? `New plugin conditionally added: \`${name}\`` - : `New plugin added: \`${name}\`` - ) - } else { - // Existing plugin, takes some more work - if (plugin.plugin && plugin.condition) { - // Multiple instances of the same plugin with different conditions - // will all be added, so we need to change the name. - if (plugins[name]?.condition) { - plugins[name + '_'] = plugin - this.store.log.info( - `Plugin \`${name}\` was conditionally added again. Renaming to ${name}_.` - ) - } else - this.store.log.info( - `Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.` - ) - } - // swap from a conditional if needed - else if (plugins[name].condition) { - plugins[name] = plugin - this.store.log.info(`Plugin \`${name}\` was explicitly added. Changing from conditional.`) - } - } - } - - this.config.plugins = { ...plugins } - - return this -} - /** * Creates a store for a set (of settings) * @@ -620,7 +403,7 @@ Pattern.prototype.__createPartWithContext = function (name, set) { const part = new Part() part.name = name part.set = set - part.stack = this.__designParts[name]?.stack || name + part.stack = this.config.parts[name]?.stack || name part.context = { parts: this.parts[set], config: this.config, @@ -700,14 +483,11 @@ Pattern.prototype.__init = function () { * This methods does that, and resolves the design config + user settings */ this.__resolveParts() // Resolves parts - .__resolveDependencies() // Resolves dependencies - .__resolveDraftOrder() // Resolves draft order + .__resolveConfig() // Gets the config from the resolver .__loadPlugins() // Loads plugins .__loadConfigData() // Makes config data available in store - .__filterOptionalMeasurements() // Removes required m's from optional list - .__loadOptionDefaults() // Merges default options with user provided ones - this.store.log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`) + this.store.log.info(`Pattern initialized. Draft order is: ${this.config.draftOrder.join(', ')}`) this.__runHooks('postInit') this.__initialized = true @@ -726,10 +506,10 @@ Pattern.prototype.__isPartHidden = function (partName) { if (Array.isArray(this.settings[this.activeSet || 0].only)) { if (this.settings[this.activeSet || 0].only.includes(partName)) return false } - if (this.__designParts?.[partName]?.hide) return true - if (this.__designParts?.[partName]?.hideAll) return true - if (this.__mutated.partHide?.[partName]) return true - if (this.__mutated.partHideAll?.[partName]) return true + if (this.config.parts?.[partName]?.hide) return true + if (this.config.parts?.[partName]?.hideAll) return true + if (this.config.partHide?.[partName]) return true + if (this.config.partHideAll?.[partName]) return true if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true return false @@ -822,40 +602,6 @@ Pattern.prototype.__loadConfigData = function () { return this } -/** - * Merges defaults for options with user-provided options - * - * @private - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__loadOptionDefaults = function () { - if (!this.config.options) this.config.options = {} - if (Object.keys(this.config.options).length < 1) return this - for (const i in this.settings) { - for (const [name, option] of Object.entries(this.config.options)) { - // Don't overwrite user-provided settings.options - if (typeof this.settings[i].options[name] === 'undefined') { - if (typeof option === 'object') { - if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100 - else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm - else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg - else if (typeof option.count !== 'undefined') - this.settings[i].options[name] = option.count - else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool - else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt - else { - let err = 'Unknown option type: ' + JSON.stringify(option) - this.store.log.error(err) - throw new Error(err) - } - } else this.settings[i].options[name] = option - } - } - } - - return this -} - /** * Loads a plugin * @@ -1049,8 +795,8 @@ Pattern.prototype.__needs = function (partName, set = 0) { // Walk the only parts, checking each one for a match in its dependencies for (const part of only) { if (part === partName) return true - if (this.__resolvedDependencies[part]) { - for (const dependency of this.__resolvedDependencies[part]) { + if (this.config.resolvedDependencies[part]) { + for (const dependency of this.config.resolvedDependencies[part]) { if (dependency === partName) return true } } @@ -1183,117 +929,11 @@ Pattern.prototype.__pack = function () { return this } -/** - * Resolves the draft order based on the configuation - * - * @private - * @param {object} graph - The object of resolved dependencies, used to call itself recursively - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDependencies) { - const sorted = Object.keys(this.__designParts).sort( - (p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1] - ) - this.__draftOrder = sorted - this.config.draftOrder = sorted - +Pattern.prototype.__resolveConfig = function () { + this.config = this.__configResolver.asConfig() return this } -Pattern.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) { - const part = depChain[0] - - this.__designParts[dependency.name] = Object.freeze(dependency) - this.__addDependency('__dependencies', part, dependency) - - depChain.forEach((c) => this.__addDependency('__resolvedDependencies', c, dependency)) - - switch (depType) { - case 'from': - this.__setFromHide(part, part.name, dependency.name) - this.__inject[part.name] = dependency.name - break - case 'after': - this.__setAfterHide(part, part.name, dependency.name) - } -} - -Pattern.prototype.__resolveMutatedPartDistance = function (partName) { - const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1 - let didChange = false - if (!this.__dependencies[partName]) return false - this.__dependencies[partName].forEach((dependency) => { - if ( - typeof this.__mutated.partDistance[dependency] === 'undefined' || - this.__mutated.partDistance[dependency] < proposed_dependent_part_distance - ) { - didChange = true - this.__mutated.partDistance[dependency] = proposed_dependent_part_distance - this.__resolveMutatedPartDistance(dependency) - } - if (DISTANCE_DEBUG) - this.store.log.debug( - `"${depType}:" partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` - ) - }) - - return didChange -} - -const depTypes = ['from', 'after'] -Pattern.prototype.__resolvePartDependencies = function (depChain, distance) { - // Resolve part Dependencies. first from then after - const part = depChain[0] - this.__resolvedDependencies[part.name] = this.__resolvedDependencies[part.name] || [] - - depTypes.forEach((d) => { - if (part[d]) { - if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`) - - const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] - - depsOfType.forEach((dot) => { - let count = Object.keys(this.__designParts).length - // if any changes resulted from resolving this part mutation - this.__resolvePartDependencyChain(depChain, dot, d) - // if a new part was added, resolve the part - const newCount = Object.keys(this.__designParts).length - if (count < newCount) { - this.__resolvePart([dot, ...depChain], distance) - count = newCount - } - }) - } - }) - - this.__resolveMutatedPartDistance(part.name) -} - -Pattern.prototype.__resolvePart = function (depChain, distance = 0) { - const part = depChain[0] - if (distance === 0) { - this.__designParts[part.name] = Object.freeze(part) - } - distance++ - if (typeof this.__mutated.partDistance[part.name] === 'undefined') { - this.__mutated.partDistance[part.name] = distance - - if (DISTANCE_DEBUG) - this.store.log.debug( - `Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__` - ) - } - - // Hide when hideAll is set - if (part.hideAll) { - this.__mutated.partHide[part.name] = true - } - - this.__resolvePartDependencies(depChain, distance) - - // add the part's config - this.__addPartConfig(part) -} /** * Resolves parts and their dependencies * @@ -1302,35 +942,15 @@ Pattern.prototype.__resolvePart = function (depChain, distance = 0) { * @param {int} distance - Keeps track of how far the dependency is from the pattern * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { - for (const part of this.designConfig.parts) { - this.__resolvePart([part], distance) - } +Pattern.prototype.__resolveParts = function () { + this.designConfig.parts.forEach((p) => this.__configResolver.addPart(p)) // Print final part distances. - for (const part of this.designConfig.parts) { - let qualifier = DISTANCE_DEBUG ? 'final' : '' - this.store.log.debug( - `⚪️ \`${part.name}\` ${qualifier} options priority is __${ - this.__mutated.partDistance[part.name] - }__` - ) - } + this.__configResolver.logPartDistances() return this } -/** - * Resolves parts depdendencies into a flat array - * - * @private - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__resolveDependencies = function () { - this.config.resolvedDependencies = this.__resolvedDependencies - return this -} - /** * Runs subscriptions to a given lifecycle hook * @@ -1364,49 +984,6 @@ Pattern.prototype.__setBase = function () { } } -/** - * Sets visibility of a dependency based on its config - * - * @private - * @param {Part} part - The part of which this is a dependency - * @param {string} name - The name of the part - * @param {string} depName - The name of the dependency - * @param {int} set - The index of the set in the list of settings - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__setFromHide = function (part, name, depName) { - if ( - part.hideDependencies || - part.hideAll || - this.__mutated.partHide[name] || - this.__mutated.partHideAll[name] - ) { - this.__mutated.partHide[depName] = true - this.__mutated.partHideAll[depName] = true - } - - return this -} - -/** - * Sets visibility of an 'after' dependency based on its config - * - * @private - * @param {Part} part - The part of which this is a dependency - * @param {string} name - The name of the part - * @param {string} depName - The name of the dependency - * @param {int} set - The index of the set in the list of settings - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__setAfterHide = function (part, name, depName) { - if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) { - this.__mutated.partHide[depName] = true - this.__mutated.partHideAll[depName] = true - } - - return this -} - /** * Returns the absolute value of a snapped percentage option * @@ -1493,22 +1070,3 @@ Pattern.prototype.__wants = function (partName, set = 0) { return true } - -////////////////////////////////////////////// -// STATIC PRIVATE FUNCTIONS // -////////////////////////////////////////////// - -/** - * Merges dependencies into a flat list - * - * @private - * @param {array} dep - New dependencies - * @param {array} current - Current dependencies - * @return {array} deps - Merged dependencies - */ -function mergeDependencies(dep = [], current = []) { - // Current dependencies - const list = [].concat(current, dep) - - return [...new Set(list)] -} diff --git a/packages/core/src/patternConfig.mjs b/packages/core/src/patternConfig.mjs new file mode 100644 index 00000000000..a28abb1b432 --- /dev/null +++ b/packages/core/src/patternConfig.mjs @@ -0,0 +1,419 @@ +import { __addNonEnumProp } from './utils.mjs' + +export function getPluginName(plugin) { + if (Array.isArray(plugin)) { + if (plugin[0].name) return plugin[0].name + if (plugin[0].plugin.name) return plugin[0].plugin.name + } else { + if (plugin.name) return plugin.name + if (plugin.plugin?.name) return plugin.plugin.name + } + + return false +} + +export function PatternConfig(pattern) { + this.pattern = pattern + this.store = pattern.store + __addNonEnumProp(this, 'plugins', { ...(pattern.designConfig.plugins || {}) }) + __addNonEnumProp(this, 'options', { ...(pattern.designConfig.options || {}) }) + __addNonEnumProp(this, 'measurements', [...(pattern.designConfig.measurements || [])]) + __addNonEnumProp(this, 'optionalMeasurements', [ + ...(pattern.designConfig.optionalMeasurements || []), + ]) + __addNonEnumProp(this, 'inject', {}) + __addNonEnumProp(this, 'directDependencies', {}) + __addNonEnumProp(this, 'resolvedDependencies', {}) + __addNonEnumProp(this, 'parts', {}) + __addNonEnumProp(this, '__resolvedParts', {}) + __addNonEnumProp(this, '__mutated', { + optionDistance: {}, + partDistance: {}, + partHide: {}, + partHideAll: {}, + }) +} + +const DISTANCE_DEBUG = false + +PatternConfig.prototype.validatePart = function (part) { + if (typeof part?.draft !== 'function') { + this.store.log.error(`Part must have a draft() method`) + return false + } + + if (!part.name) { + this.store.log.error(`Part must have a name`) + return false + } + + return true +} +PatternConfig.prototype.addPart = function (part) { + if (this.validatePart(part)) this.__resolvePart([part]) + + return this +} + +PatternConfig.prototype.logPartDistances = function () { + for (const partName in this.parts) { + let qualifier = DISTANCE_DEBUG ? 'final' : '' + this.store.log.debug( + `⚪️ \`${partName}\` ${qualifier} options priority is __${this.__mutated.partDistance[partName]}__` + ) + } +} + +PatternConfig.prototype.asConfig = function () { + return { + parts: this.parts, + plugins: this.plugins, + measurements: this.measurements, + options: this.options, + optionalMeasurements: this.optionalMeasurements, + resolvedDependencies: this.resolvedDependencies, + directDependencies: this.directDependencies, + inject: this.inject, + draftOrder: this.__resolveDraftOrder(), + partHide: this.__mutated.partHide, + partHideAll: this.__mutated.partHideAll, + } +} + +PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) { + const part = depChain[0] + if (distance === 0) { + this.parts[part.name] = Object.freeze(part) + } + distance++ + if (typeof this.__mutated.partDistance[part.name] === 'undefined') { + this.__mutated.partDistance[part.name] = distance + + if (DISTANCE_DEBUG) + this.store.log.debug( + `Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__` + ) + } + + // Hide when hideAll is set + if (part.hideAll) { + this.__mutated.partHide[part.name] = true + } + + this.__resolvePartDependencies(depChain, distance) + + // add the part's config + this.__addPartConfig(part) +} + +/** + * Resolves/Adds a part's design configuration to the pattern config + * + * @private + * @param {Part} part - The part of which to resolve the config + * @param {onject} config - The global config + * @param {Store} store - The store, used for logging + * @return {object} config - The mutated global config + */ +PatternConfig.prototype.__addPartConfig = function (part) { + if (this.__resolvedParts[part.name]) return this + + // Add parts, using set to keep them unique in the array + // this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)] + + return this.__addPartOptions(part) + .__addPartMeasurements(part, true) + .__addPartMeasurements(part, false) + .__addPartPlugins(part) +} + +/** + * Resolves/Adds a part's configured options to the global config + * + * @private + * @param {Part} part - The part of which to resolve the config + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__addPartOptions = function (part) { + if (!part.options) return this + + const partDistance = this.__mutated.partDistance?.[part.name] || 0 + for (const optionName in part.options) { + const option = part.options[optionName] + const optionDistance = this.__mutated.optionDistance[optionName] + if (optionDistance && DISTANCE_DEBUG) + this.store.log.debug( + `optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__` + ) + if (!optionDistance || optionDistance > partDistance) { + this.__mutated.optionDistance[optionName] = partDistance + // Keep design parts immutable in the pattern or risk subtle bugs + this.options[optionName] = Object.freeze(option) + this.store.log.debug( + optionDistance + ? `🟣 __${optionName}__ option overwritten by \`${part.name}\`` + : `🔵 __${optionName}__ option loaded from part \`${part.name}\`` + ) + this.__loadOptionDefault(optionName, option) + } + } + + return this +} + +/** + * Resolves/Adds a part's configured measurements to the global config + * + * @private + * @param {Part} part - The part of which to resolve the config + * @param {array} list - The list of resolved measurements + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__addPartMeasurements = function (part, optional = false) { + const listType = optional ? 'optionalMeasurements' : 'measurements' + if (part[listType]) { + part[listType].forEach((m) => { + const isInReqList = this.measurements.indexOf(m) !== -1 + const optInd = this.optionalMeasurements.indexOf(m) + const isInOptList = optInd !== -1 + + if (isInReqList) return + if (optional && !isInOptList) this.optionalMeasurements.push(m) + if (!optional) { + this.measurements.push(m) + + if (isInOptList) this.optionalMeasurements.splice(optInd, 1) + } + + this.store.log.debug( + `🟠 __${m}__ measurement is ${optional ? 'optional' : 'required'} in \`${part.name}\`` + ) + }) + } + + return this +} + +/** + * Resolves/Adds a part's configured plugins to the global config + * + * @private + * @param {Part} part - The part of which to resolve the config + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__addPartPlugins = function (part) { + if (!part.plugins) return this + + const plugins = this.plugins + // Side-step immutability of the part object to ensure plugins is an array + let partPlugins = part.plugins + if (!Array.isArray(partPlugins)) partPlugins = [partPlugins] + // Go through list of part plugins + for (let plugin of partPlugins) { + const name = getPluginName(plugin) + this.store.log.debug( + plugin.plugin + ? `🔌 Resolved __${name}__ conditional plugin in \`${part.name}\`` + : `🔌 Resolved __${name}__ plugin in \`${part.name}\`` + ) + // Handle [plugin, data] scenario + if (Array.isArray(plugin)) { + const pluginObj = { ...plugin[0], data: plugin[1] } + plugin = pluginObj + } + if (!plugins[name]) { + // New plugin, so we load it + plugins[name] = plugin + this.store.log.info( + plugin.condition + ? `New plugin conditionally added: \`${name}\`` + : `New plugin added: \`${name}\`` + ) + } else { + // Existing plugin, takes some more work + if (plugin.plugin && plugin.condition) { + // Multiple instances of the same plugin with different conditions + // will all be added, so we need to change the name. + if (plugins[name]?.condition) { + plugins[name + '_'] = plugin + this.store.log.info( + `Plugin \`${name}\` was conditionally added again. Renaming to ${name}_.` + ) + } else + this.store.log.info( + `Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.` + ) + } + // swap from a conditional if needed + else if (plugins[name].condition) { + plugins[name] = plugin + this.store.log.info(`Plugin \`${name}\` was explicitly added. Changing from conditional.`) + } + } + } + + return this +} + +PatternConfig.prototype.__loadOptionDefault = function (optionName, option) { + this.pattern.settings.forEach((set) => { + if (typeof set.options[optionName] !== 'undefined') return + if (typeof option === 'object') { + if (typeof option.pct !== 'undefined') set.options[optionName] = option.pct / 100 + else if (typeof option.mm !== 'undefined') set.options[optionName] = option.mm + else if (typeof option.deg !== 'undefined') set.options[optionName] = option.deg + else if (typeof option.count !== 'undefined') set.options[optionName] = option.count + else if (typeof option.bool !== 'undefined') set.options[optionName] = option.bool + else if (typeof option.dflt !== 'undefined') set.options[optionName] = option.dflt + else { + let err = 'Unknown option type: ' + JSON.stringify(option) + this.store.log.error(err) + throw new Error(err) + } + } else set.options[optionName] = option + }) +} + +PatternConfig.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) { + const part = depChain[0] + + this.parts[dependency.name] = Object.freeze(dependency) + this.__addDependency('directDependencies', part, dependency) + + depChain.forEach((c) => this.__addDependency('resolvedDependencies', c, dependency)) + + switch (depType) { + case 'from': + this.__setFromHide(part, part.name, dependency.name) + this.inject[part.name] = dependency.name + break + case 'after': + this.__setAfterHide(part, part.name, dependency.name) + } +} + +PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) { + const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1 + let didChange = false + if (!this.directDependencies[partName]) return false + this.directDependencies[partName].forEach((dependency) => { + if ( + typeof this.__mutated.partDistance[dependency] === 'undefined' || + this.__mutated.partDistance[dependency] < proposed_dependent_part_distance + ) { + didChange = true + this.__mutated.partDistance[dependency] = proposed_dependent_part_distance + this.__resolveMutatedPartDistance(dependency) + } + if (DISTANCE_DEBUG) + this.store.log.debug( + `partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` + ) + }) + + return didChange +} + +const depTypes = ['from', 'after'] +PatternConfig.prototype.__resolvePartDependencies = function (depChain, distance) { + // Resolve part Dependencies. first from then after + const part = depChain[0] + this.resolvedDependencies[part.name] = this.resolvedDependencies[part.name] || [] + + depTypes.forEach((d) => { + if (part[d]) { + if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`) + + const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] + + depsOfType.forEach((dot) => { + let count = Object.keys(this.parts).length + // if any changes resulted from resolving this part mutation + this.__resolvePartDependencyChain(depChain, dot, d) + // if a new part was added, resolve the part + const newCount = Object.keys(this.parts).length + if (count < newCount) { + this.__resolvePart([dot, ...depChain], distance) + count = newCount + } + }) + } + }) + + this.__resolveMutatedPartDistance(part.name) +} + +/** + * Adds a part as a simple dependency + * + * @private + * @param {string} name - The name of the dependency + * @param {object} part - The part configuration + * @param {object} dep - The dependency configuration + * @return {object} this - The Pattern instance + */ +PatternConfig.prototype.__addDependency = function (dependencyList, part, dep) { + this[dependencyList][part.name] = this[dependencyList][part.name] || [] + if (dependencyList == 'resolvedDependencies' && DISTANCE_DEBUG) + this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`) + if (this[dependencyList][part.name].indexOf(dep.name) === -1) + this[dependencyList][part.name].push(dep.name) +} + +/** + * Resolves the draft order based on the configuation + * + * @private + * @param {object} graph - The object of resolved dependencies, used to call itself recursively + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__resolveDraftOrder = function () { + this.__draftOrder = Object.keys(this.parts).sort( + (p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1] + ) + + return this.__draftOrder +} + +/** + * Sets visibility of a dependency based on its config + * + * @private + * @param {Part} part - The part of which this is a dependency + * @param {string} name - The name of the part + * @param {string} depName - The name of the dependency + * @param {int} set - The index of the set in the list of settings + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__setFromHide = function (part, name, depName) { + if ( + part.hideDependencies || + part.hideAll || + this.__mutated.partHide[name] || + this.__mutated.partHideAll[name] + ) { + this.__mutated.partHide[depName] = true + this.__mutated.partHideAll[depName] = true + } + + return this +} + +/** + * Sets visibility of an 'after' dependency based on its config + * + * @private + * @param {Part} part - The part of which this is a dependency + * @param {string} name - The name of the part + * @param {string} depName - The name of the dependency + * @param {int} set - The index of the set in the list of settings + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__setAfterHide = function (part, name, depName) { + if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) { + this.__mutated.partHide[depName] = true + this.__mutated.partHideAll[depName] = true + } + + return this +} diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index 8f5c76621d7..036d91e89bd 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -32,12 +32,12 @@ describe('Pattern', () => { 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.__inject).to.equal('object') - expect(typeof pattern.__dependencies).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(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('') @@ -145,7 +145,7 @@ describe('Pattern', () => { }) it('Pattern.__init() should resolve parts', () => { - expect(pattern.designConfig.parts.length).to.equal(3) + expect(Object.keys(pattern.config.parts)).to.have.lengthOf(3) }) it('Pattern.__init() should resolve plugins', () => { @@ -324,14 +324,14 @@ describe('Pattern', () => { expect(pattern.config.options.optionR.list[1]).to.equal('green') expect(pattern.config.options.optionR.list[2]).to.equal('blue') // Dependencies - expect(pattern.__dependencies.partB).to.include('partA') - expect(pattern.__dependencies.partC).to.include('partB') - expect(pattern.__dependencies.partR).to.include('partC') - expect(pattern.__dependencies.partR).to.include('partA') + expect(pattern.config.directDependencies.partB).to.include('partA') + expect(pattern.config.directDependencies.partC).to.include('partB') + expect(pattern.config.directDependencies.partR).to.include('partC') + expect(pattern.config.directDependencies.partR).to.include('partA') // Inject - expect(pattern.__inject.partB).to.equal('partA') - expect(pattern.__inject.partC).to.equal('partB') - expect(pattern.__inject.partR).to.equal('partA') + expect(pattern.config.inject.partB).to.equal('partA') + expect(pattern.config.inject.partC).to.equal('partB') + expect(pattern.config.inject.partR).to.equal('partA') // Draft order expect(pattern.config.draftOrder[0]).to.equal('partA') expect(pattern.config.draftOrder[1]).to.equal('partB') @@ -472,12 +472,12 @@ describe('Pattern', () => { expect(pattern.config.options.optionD.list[1]).to.equal('green') expect(pattern.config.options.optionD.list[2]).to.equal('blue') // Dependencies - expect(pattern.__dependencies.partB[0]).to.equal('partA') - expect(pattern.__dependencies.partC[0]).to.equal('partB') - expect(pattern.__dependencies.partD[0]).to.equal('partC') + expect(pattern.config.directDependencies.partB[0]).to.equal('partA') + expect(pattern.config.directDependencies.partC[0]).to.equal('partB') + expect(pattern.config.directDependencies.partD[0]).to.equal('partC') // Inject - expect(pattern.__inject.partB).to.equal('partA') - expect(pattern.__inject.partC).to.equal('partB') + expect(pattern.config.inject.partB).to.equal('partA') + expect(pattern.config.inject.partC).to.equal('partB') // Draft order expect(pattern.config.draftOrder[0]).to.equal('partA') expect(pattern.config.draftOrder[1]).to.equal('partB') diff --git a/packages/core/tests/pattern-runtime-parts.test.mjs b/packages/core/tests/pattern-runtime-parts.test.mjs index a3a640e6721..dbe71799dbb 100644 --- a/packages/core/tests/pattern-runtime-parts.test.mjs +++ b/packages/core/tests/pattern-runtime-parts.test.mjs @@ -12,31 +12,31 @@ describe('Pattern', () => { const part2 = { name: 'test2', - from: part1, + after: part1, draft: ({ part }) => part, } const part3 = { name: 'test3', - after: part2, + from: part2, draft: ({ part }) => part, } describe('with resolveImmediately: true', () => { - it('Should add the part to the internal part object', () => { + it('Should add the part to parts object', () => { const design = new Design({ parts: [part1] }) const pattern = new design() pattern.__init() pattern.addPart(part2, true) - expect(pattern.__designParts.test2).to.equal(part2) + expect(pattern.config.parts.test2).to.equal(part2) }) it('Should resolve injected dependencies for the new part', () => { const design = new Design({ parts: [part1] }) const pattern = new design() pattern.__init() - pattern.addPart(part2, true) - expect(pattern.__inject.test2).to.equal('test') + pattern.addPart(part3, true) + expect(pattern.config.inject.test3).to.equal('test2') }) it('Should resolve all dependencies for the new part', () => { @@ -45,7 +45,7 @@ describe('Pattern', () => { pattern.__init() pattern.addPart(part3, true) expect(pattern.config.resolvedDependencies.test3).to.have.members(['test', 'test2']) - expect(pattern.__designParts.test2).to.equal(part2) + expect(pattern.config.parts.test2).to.equal(part2) }) it('Should add a the measurements for the new part', () => { @@ -79,7 +79,7 @@ describe('Pattern', () => { expect(pattern.config.plugins.testPlugin).to.equal(plugin) }) - it('Should add the options for the new part', () => { + it('Should resolve the options for the new part', () => { const design = new Design({ parts: [part1] }) const pattern = new design() pattern.__init() @@ -96,6 +96,61 @@ describe('Pattern', () => { pattern.addPart(part2, true) expect(pattern.config.options.opt1).to.equal(opt1) }) + + it('Should resolve the dependency options for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const opt1 = { pct: 10, min: 0, max: 50 } + const part2 = { + name: 'test2', + options: { + opt1, + }, + draft: ({ part }) => part, + } + + const part3 = { + name: 'test3', + from: part2, + draft: ({ part }) => part, + } + + pattern.addPart(part3, true) + expect(pattern.config.options.opt1).to.equal(opt1) + }) + + it('Should resolve the overwritten options for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const opt1 = { pct: 10, min: 0, max: 50 } + const part2 = { + name: 'test2', + options: { + opt1: { pct: 15, min: 10, max: 55 }, + }, + draft: ({ part }) => part, + } + + const part3 = { + name: 'test3', + from: part2, + options: { + opt1, + }, + draft: ({ part }) => part, + } + + pattern.addPart(part3, true) + expect(pattern.config.options.opt1).to.equal(opt1) + }) + }) + + describe('with resolveImmediately: false', () => { + it('does not create duplications in the configuration') }) }) }) diff --git a/packages/core/tests/snap.test.mjs b/packages/core/tests/snap.test.mjs index fa64a1f9f6b..4c51de52472 100644 --- a/packages/core/tests/snap.test.mjs +++ b/packages/core/tests/snap.test.mjs @@ -42,6 +42,7 @@ describe('Snapped options', () => { snap: [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144], }, }, + draft: ({ part }) => part, } const design = new Design({ parts: [part] }) const patternA = new design({ options: { test: 0.13 }, measurements }).draft() @@ -67,6 +68,7 @@ describe('Snapped options', () => { }, }, }, + draft: ({ part }) => part, } const design = new Design({ parts: [part] }) const patternA = new design({ options: { test: 0.13 }, measurements, units: 'metric' }).draft() @@ -94,6 +96,7 @@ describe('Snapped options', () => { }, }, }, + draft: ({ part }) => part, } const design = new Design({ parts: [part] }) const patternA = new design({ From 0dab926ee71bfbf109913490f0454c1741f82d4f Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Wed, 22 Feb 2023 16:09:43 +0200 Subject: [PATCH 08/67] document and refactor --- packages/core/src/pattern.mjs | 71 ++++-- packages/core/src/patternConfig.mjs | 353 +++++++++++++++++----------- packages/core/src/store.mjs | 2 +- 3 files changed, 260 insertions(+), 166 deletions(-) diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index d593c360304..6c4ce537588 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -26,6 +26,12 @@ import cloneDeep from 'lodash.clonedeep' * @return {object} this - The Pattern instance */ export function Pattern(designConfig = {}) { + // Enumerable properties + this.designConfig = cloneDeep(designConfig) // The design configuration (unresolved) + this.config = {} // Will hold the resolved pattern after calling __init() + this.store = new Store() // Pattern-wide store + this.setStores = [] // Per-set stores + // Non-enumerable properties __addNonEnumProp(this, 'plugins', {}) __addNonEnumProp(this, 'width', 0) @@ -42,13 +48,6 @@ export function Pattern(designConfig = {}) { __addNonEnumProp(this, 'config.parts', {}) __addNonEnumProp(this, 'config.resolvedDependencies', {}) __addNonEnumProp(this, '__storeMethods', new Set()) - __addNonEnumProp(this, '__hide', {}) - - // Enumerable properties - this.designConfig = cloneDeep(designConfig) // The design configuration (unresolved) - this.config = {} // Will hold the resolved pattern after calling __init() - this.store = new Store() // Pattern-wide store - this.setStores = [] // Per-set stores __addNonEnumProp(this, '__configResolver', new PatternConfig(this)) // handles config resolution during __init() as well as runtime part adding return this @@ -59,9 +58,12 @@ export function Pattern(designConfig = {}) { ////////////////////////////////////////////// /** - * FIXME: Allows adding parts to the config at runtime + * Allows adding parts to the config at runtime * * @param {object} part - The part to add + * @param {boolean} resolveImmediately - Should the part be resolved now, or wait until the next call to {@link __init()}? + * It is useful to resolve immediately if one part is being added at runtime + * It might be useful to not resolve immediately if a number of parts will be added over multiple calls * @return {object} this - The Pattern instance */ Pattern.prototype.addPart = function (part, resolveImmediately = false) { @@ -443,24 +445,6 @@ Pattern.prototype.__createStackWithContext = function (name) { return stack } -/** - * Filter optional measurements out id they are also required measurments - * - * @private - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__filterOptionalMeasurements = function () { - if (!this.config.optionalMeasurements) { - this.config.optionalMeasurements = [] - return this - } - this.config.optionalMeasurements = this.config.optionalMeasurements.filter( - (m) => this.config.measurements.indexOf(m) === -1 - ) - - return this -} - /** * Initializes the pattern coniguration and settings * @@ -484,6 +468,7 @@ 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 @@ -602,6 +587,40 @@ Pattern.prototype.__loadConfigData = function () { return this } +/** + * Merges defaults for options with user-provided options + * + * @private + * @return {Pattern} this - The Pattern instance + */ +Pattern.prototype.__loadOptionDefaults = function () { + if (!this.config.options) this.config.options = {} + if (Object.keys(this.config.options).length < 1) return this + for (const i in this.settings) { + for (const [name, option] of Object.entries(this.config.options)) { + // Don't overwrite user-provided settings.options + if (typeof this.settings[i].options[name] === 'undefined') { + if (typeof option === 'object') { + if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100 + else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm + else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg + else if (typeof option.count !== 'undefined') + this.settings[i].options[name] = option.count + else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool + else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt + else { + let err = 'Unknown option type: ' + JSON.stringify(option) + this.store.log.error(err) + throw new Error(err) + } + } else this.settings[i].options[name] = option + } + } + } + + return this +} + /** * Loads a plugin * diff --git a/packages/core/src/patternConfig.mjs b/packages/core/src/patternConfig.mjs index a28abb1b432..79fa1083cb4 100644 --- a/packages/core/src/patternConfig.mjs +++ b/packages/core/src/patternConfig.mjs @@ -1,41 +1,72 @@ import { __addNonEnumProp } from './utils.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) { - if (Array.isArray(plugin)) { - if (plugin[0].name) return plugin[0].name - if (plugin[0].plugin.name) return plugin[0].plugin.name - } else { - if (plugin.name) return plugin.name - if (plugin.plugin?.name) return plugin.plugin.name - } + const toCheck = Array.isArray(plugin) ? plugin[0] : plugin + return toCheck.name || toCheck.plugin?.name || false return false } +///////////////// +// CONSTRUCTOR // +///////////////// +/** + * A class for handling config resolution for a Pattern + * @class + * @param {Pattern} pattern the pattern whose config is being handled + */ export function PatternConfig(pattern) { - this.pattern = pattern + /** @type {Store} the pattern's store, for logging */ this.store = pattern.store - __addNonEnumProp(this, 'plugins', { ...(pattern.designConfig.plugins || {}) }) - __addNonEnumProp(this, 'options', { ...(pattern.designConfig.options || {}) }) - __addNonEnumProp(this, 'measurements', [...(pattern.designConfig.measurements || [])]) - __addNonEnumProp(this, 'optionalMeasurements', [ - ...(pattern.designConfig.optionalMeasurements || []), - ]) - __addNonEnumProp(this, 'inject', {}) - __addNonEnumProp(this, 'directDependencies', {}) - __addNonEnumProp(this, 'resolvedDependencies', {}) - __addNonEnumProp(this, 'parts', {}) + + /** @type {Object} resolved plugins keyed by name */ + this.plugins = { ...(pattern.designConfig.plugins || {}) } + /** @type {Object} resolved options keyed by name */ + this.options = { ...(pattern.designConfig.options || {}) } + /** @type {string[]} required measurements */ + this.measurements = [...(pattern.designConfig.measurements || [])] + /** @type {string[]} optional measurements */ + this.optionalMeasurements = [...(pattern.designConfig.optionalMeasurements || [])] + /** @type {Object} the names of the parts that will be injected */ + this.inject = {} + /** @type {Object} arrays of parts that are direct dependencies of the key */ + this.directDependencies = {} + /** @type {Object} arrays of all dependencies of the key */ + this.resolvedDependencies = {} + /** @type {Object} parts to include in the pattern */ + this.parts = {} + /** @type {Object} which parts are hidden */ + this.partHide = {} + /** @type {Object} which parts hide all their dependencies */ + this.partHideAll = {} + + /** to track which parts have already been resolved */ __addNonEnumProp(this, '__resolvedParts', {}) + /** @type {Object} to track when to overwrite options */ __addNonEnumProp(this, '__mutated', { optionDistance: {}, partDistance: {}, - partHide: {}, - partHideAll: {}, }) } +/** @type {Boolean} change me to true to get full debugging of the resolution process */ const DISTANCE_DEBUG = false +//////////////////// +// PUBLIC METHODs // +//////////////////// + +/** + * Validate that a part meets the requirements to be added to the pattern + * @param {Object} part a part configuration + * @return {boolean} whether the part is valid + */ PatternConfig.prototype.validatePart = function (part) { if (typeof part?.draft !== 'function') { this.store.log.error(`Part must have a draft() method`) @@ -49,12 +80,18 @@ PatternConfig.prototype.validatePart = function (part) { return true } + +/** + * Chainable method to add a part to the configuration + * @param {Object} part + */ PatternConfig.prototype.addPart = function (part) { - if (this.validatePart(part)) this.__resolvePart([part]) + if (this.validatePart(part)) this.__addPart([part]) return this } +/** Log the final report on part inheritance order */ PatternConfig.prototype.logPartDistances = function () { for (const partName in this.parts) { let qualifier = DISTANCE_DEBUG ? 'final' : '' @@ -64,6 +101,10 @@ PatternConfig.prototype.logPartDistances = function () { } } +/** + * Return a configuration in the structure expected by the pattern + * @return {Object} contains parts, plugins, measurements, options, optionalMeasurements, resolvedDependencies, directDependencies, inject, draftOrder, partHide, and partHideAll + */ PatternConfig.prototype.asConfig = function () { return { parts: this.parts, @@ -75,17 +116,30 @@ PatternConfig.prototype.asConfig = function () { directDependencies: this.directDependencies, inject: this.inject, draftOrder: this.__resolveDraftOrder(), - partHide: this.__mutated.partHide, - partHideAll: this.__mutated.partHideAll, + partHide: this.partHide, + partHideAll: this.partHideAll, } } -PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) { +///////////////////// +// PRIVATE METHODS // +///////////////////// + +/** + * Add a part's configuration + * Uses recursion to also add that part's dependencies + * @private + * @param {Object[]} depChain an array starting with the current part to add and containing its dependents/descendents in order + */ +PatternConfig.prototype.__addPart = function (depChain) { + // the current part is the head of the chain const part = depChain[0] - if (distance === 0) { + // the longer the chain, the deeper the part is down it + const distance = depChain.length + if (!this.parts[part.name]) { this.parts[part.name] = Object.freeze(part) } - distance++ + // if it hasn't been registered with a distance, do that now if (typeof this.__mutated.partDistance[part.name] === 'undefined') { this.__mutated.partDistance[part.name] = distance @@ -97,10 +151,11 @@ PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) { // Hide when hideAll is set if (part.hideAll) { - this.__mutated.partHide[part.name] = true + this.partHide[part.name] = true } - this.__resolvePartDependencies(depChain, distance) + // resolve its dependencies + this.__resolvePartDependencies(depChain) // add the part's config this.__addPartConfig(part) @@ -111,20 +166,16 @@ PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) { * * @private * @param {Part} part - The part of which to resolve the config - * @param {onject} config - The global config - * @param {Store} store - The store, used for logging - * @return {object} config - The mutated global config + * @return this */ PatternConfig.prototype.__addPartConfig = function (part) { + // don't resolve a part that's already been resolved if (this.__resolvedParts[part.name]) return this - // Add parts, using set to keep them unique in the array - // this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)] - - return this.__addPartOptions(part) - .__addPartMeasurements(part, true) - .__addPartMeasurements(part, false) - .__addPartPlugins(part) + return this.__addPartOptions(part) // add options + .__addPartMeasurements(part, false) // add required measurements + .__addPartMeasurements(part, true) // add optional measurements + .__addPartPlugins(part) // add plugins } /** @@ -132,29 +183,38 @@ PatternConfig.prototype.__addPartConfig = function (part) { * * @private * @param {Part} part - The part of which to resolve the config - * @return {Pattern} this - The Pattern instance + * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartOptions = function (part) { + // skip empty options if (!part.options) return this + // get the part's option priority const partDistance = this.__mutated.partDistance?.[part.name] || 0 + + // loop through options for (const optionName in part.options) { const option = part.options[optionName] + // get the priority of this option's current registration const optionDistance = this.__mutated.optionDistance[optionName] + // debug the comparison if (optionDistance && DISTANCE_DEBUG) this.store.log.debug( `optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__` ) + + // if it's never been registered, or it's registered at a further distance if (!optionDistance || optionDistance > partDistance) { - this.__mutated.optionDistance[optionName] = partDistance - // Keep design parts immutable in the pattern or risk subtle bugs + // Keep options immutable in the pattern or risk subtle bugs this.options[optionName] = Object.freeze(option) + // register the new distance + this.__mutated.optionDistance[optionName] = partDistance + // debug appropriately this.store.log.debug( optionDistance ? `🟣 __${optionName}__ option overwritten by \`${part.name}\`` : `🔵 __${optionName}__ option loaded from part \`${part.name}\`` ) - this.__loadOptionDefault(optionName, option) } } @@ -166,22 +226,32 @@ PatternConfig.prototype.__addPartOptions = function (part) { * * @private * @param {Part} part - The part of which to resolve the config - * @param {array} list - The list of resolved measurements - * @return {Pattern} this - The Pattern instance + * @param {boolean} optional - are these measurements optional? + * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartMeasurements = function (part, optional = false) { + // which list are we drawing from? const listType = optional ? 'optionalMeasurements' : 'measurements' + // if the part has measurements of this type, go through them if (part[listType]) { part[listType].forEach((m) => { + // we need to know what lists it's already present on const isInReqList = this.measurements.indexOf(m) !== -1 + // if it's already registered as required, we're done here + if (isInReqList) return + + // check if it's registered as optional const optInd = this.optionalMeasurements.indexOf(m) const isInOptList = optInd !== -1 - if (isInReqList) return + // if it is optional and not in the list, push it if (optional && !isInOptList) this.optionalMeasurements.push(m) + // if it's not optional if (!optional) { + // push it to required list this.measurements.push(m) + // make sure it's not also registered as optional if (isInOptList) this.optionalMeasurements.splice(optInd, 1) } @@ -199,7 +269,7 @@ PatternConfig.prototype.__addPartMeasurements = function (part, optional = false * * @private * @param {Part} part - The part of which to resolve the config - * @return {Pattern} this - The Pattern instance + * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartPlugins = function (part) { if (!part.plugins) return this @@ -255,109 +325,117 @@ PatternConfig.prototype.__addPartPlugins = function (part) { return this } -PatternConfig.prototype.__loadOptionDefault = function (optionName, option) { - this.pattern.settings.forEach((set) => { - if (typeof set.options[optionName] !== 'undefined') return - if (typeof option === 'object') { - if (typeof option.pct !== 'undefined') set.options[optionName] = option.pct / 100 - else if (typeof option.mm !== 'undefined') set.options[optionName] = option.mm - else if (typeof option.deg !== 'undefined') set.options[optionName] = option.deg - else if (typeof option.count !== 'undefined') set.options[optionName] = option.count - else if (typeof option.bool !== 'undefined') set.options[optionName] = option.bool - else if (typeof option.dflt !== 'undefined') set.options[optionName] = option.dflt - else { - let err = 'Unknown option type: ' + JSON.stringify(option) - this.store.log.error(err) - throw new Error(err) - } - } else set.options[optionName] = option - }) -} - -PatternConfig.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) { - const part = depChain[0] - - this.parts[dependency.name] = Object.freeze(dependency) - this.__addDependency('directDependencies', part, dependency) - - depChain.forEach((c) => this.__addDependency('resolvedDependencies', c, dependency)) - - switch (depType) { - case 'from': - this.__setFromHide(part, part.name, dependency.name) - this.inject[part.name] = dependency.name - break - case 'after': - this.__setAfterHide(part, part.name, dependency.name) - } -} - -PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) { - const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1 - let didChange = false - if (!this.directDependencies[partName]) return false - this.directDependencies[partName].forEach((dependency) => { - if ( - typeof this.__mutated.partDistance[dependency] === 'undefined' || - this.__mutated.partDistance[dependency] < proposed_dependent_part_distance - ) { - didChange = true - this.__mutated.partDistance[dependency] = proposed_dependent_part_distance - this.__resolveMutatedPartDistance(dependency) - } - if (DISTANCE_DEBUG) - this.store.log.debug( - `partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` - ) - }) - - return didChange -} - +// the two types of dependencies const depTypes = ['from', 'after'] -PatternConfig.prototype.__resolvePartDependencies = function (depChain, distance) { - // Resolve part Dependencies. first from then after + +/** + * Recursively register part dependencies + * triggers {@link __addPart} on new parts found during resolution + * @param {Object[]} depChain an array starting with the current part to register and containing its dependents/descendents in order + * @return {PatternConfig} this + * @private + */ +PatternConfig.prototype.__resolvePartDependencies = function (depChain) { + // the current part is the head of the chain const part = depChain[0] + // get or make its array of resolved dependencies this.resolvedDependencies[part.name] = this.resolvedDependencies[part.name] || [] + // for each dependency type (from, after) depTypes.forEach((d) => { + // if the part has dependencies of that type if (part[d]) { if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`) + // enforce an array const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] + // each dependency depsOfType.forEach((dot) => { - let count = Object.keys(this.parts).length - // if any changes resulted from resolving this part mutation - this.__resolvePartDependencyChain(depChain, dot, d) - // if a new part was added, resolve the part - const newCount = Object.keys(this.parts).length - if (count < newCount) { - this.__resolvePart([dot, ...depChain], distance) - count = newCount + // add it as a direct dependency of the current part + this.__addDependency('directDependencies', part.name, dot.name) + // add it as a resolved dependency of all parts in the chain + depChain.forEach((c) => this.__addDependency('resolvedDependencies', c.name, dot.name)) + + // handle hiding and injecting + this.__handlePartDependencyOfType(part, dot.name, d) + + // if the dependency isn't registered, register it + if (!this.parts[dot.name]) { + // add the part's configuration + this.__addPart([dot, ...depChain]) } }) } }) + // now that the chain has been registered, recalculate the part distances this.__resolveMutatedPartDistance(part.name) } /** - * Adds a part as a simple dependency - * + * Adds a part as either a direct or a resolved dependency + * @param {string} dependencyList which list to add the part to, 'resolvedDependencies' or 'directDependencies' + * @param {string} partName the name of the part to add the dependency to in the list + * @param {string} depName the name of the dependency to add to the list * @private - * @param {string} name - The name of the dependency - * @param {object} part - The part configuration - * @param {object} dep - The dependency configuration - * @return {object} this - The Pattern instance */ -PatternConfig.prototype.__addDependency = function (dependencyList, part, dep) { - this[dependencyList][part.name] = this[dependencyList][part.name] || [] +PatternConfig.prototype.__addDependency = function (dependencyList, partName, depName) { + this[dependencyList][partName] = this[dependencyList][partName] || [] if (dependencyList == 'resolvedDependencies' && DISTANCE_DEBUG) - this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`) - if (this[dependencyList][part.name].indexOf(dep.name) === -1) - this[dependencyList][part.name].push(dep.name) + this.store.log.debug(`add ${depName} to ${partName} dependencyResolution`) + if (this[dependencyList][partName].indexOf(depName) === -1) + this[dependencyList][partName].push(depName) +} + +/** + * Handle dependency-type specific config business + * @param {Object} part the part to add the dependency to + * @param {string} depName the name of the dependency to add + * @param {string} depType the type of dependency, 'from' or 'after' + * @private + */ +PatternConfig.prototype.__handlePartDependencyOfType = function (part, depName, depType) { + switch (depType) { + case 'from': + this.__setFromHide(part, depName) + this.inject[part.name] = depName + break + case 'after': + this.__setAfterHide(part, depName) + } +} + +/** + * Resolve part option priority + * Recursively bumps priorities down the dependency chain + * @param {string} partName the name of the part to resolve + * @private + */ +PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) { + // if the part has no dependencies, bail + if (!this.directDependencies[partName]) return + + // propose that each of the part's direct dependencies should be at a distance 1 further than the part's distance + const proposed_dependency_distance = this.__mutated.partDistance[partName] + 1 + // check each direct dependency + this.directDependencies[partName].forEach((dependency) => { + // if the dependency doesn't have a distance, or that distance is less than the proposal + if ( + typeof this.__mutated.partDistance[dependency] === 'undefined' || + this.__mutated.partDistance[dependency] < proposed_dependency_distance + ) { + // set the new distance + this.__mutated.partDistance[dependency] = proposed_dependency_distance + // bump the dependency's dependencies as well + this.__resolveMutatedPartDistance(dependency) + } + + if (DISTANCE_DEBUG) + this.store.log.debug( + `partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` + ) + }) } /** @@ -376,24 +454,22 @@ PatternConfig.prototype.__resolveDraftOrder = function () { } /** - * Sets visibility of a dependency based on its config + * Sets visibility of a 'from' dependency based on its config * * @private * @param {Part} part - The part of which this is a dependency - * @param {string} name - The name of the part * @param {string} depName - The name of the dependency - * @param {int} set - The index of the set in the list of settings * @return {Pattern} this - The Pattern instance */ -PatternConfig.prototype.__setFromHide = function (part, name, depName) { +PatternConfig.prototype.__setFromHide = function (part, depName) { if ( part.hideDependencies || part.hideAll || - this.__mutated.partHide[name] || - this.__mutated.partHideAll[name] + this.partHide[part.name] || + this.partHideAll[part.name] ) { - this.__mutated.partHide[depName] = true - this.__mutated.partHideAll[depName] = true + this.partHide[depName] = true + this.partHideAll[depName] = true } return this @@ -404,15 +480,14 @@ PatternConfig.prototype.__setFromHide = function (part, name, depName) { * * @private * @param {Part} part - The part of which this is a dependency - * @param {string} name - The name of the part * @param {string} depName - The name of the dependency * @param {int} set - The index of the set in the list of settings * @return {Pattern} this - The Pattern instance */ -PatternConfig.prototype.__setAfterHide = function (part, name, depName) { - if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) { - this.__mutated.partHide[depName] = true - this.__mutated.partHideAll[depName] = true +PatternConfig.prototype.__setAfterHide = function (part, depName) { + if (this.partHide[part.name] || this.partHideAll[part.name]) { + this.partHide[depName] = true + this.partHideAll[depName] = true } return this diff --git a/packages/core/src/store.mjs b/packages/core/src/store.mjs index c8f5fad5fbd..f88f0e5f343 100644 --- a/packages/core/src/store.mjs +++ b/packages/core/src/store.mjs @@ -38,7 +38,7 @@ export function Store(methods = []) { logs.warning.push(...data) }, error: function (...data) { - console.error(...data) + if (typeof window !== 'undefined') console.error(...data) logs.error.push(...data) }, } From 239eff81c038833ad377690b51fed48949e604a8 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Mon, 20 Feb 2023 06:08:07 +0200 Subject: [PATCH 09/67] begin refactoring part resolution to handle one part at a time --- packages/core/src/pattern.mjs | 220 +++++++++------------- packages/core/tests/pattern-init.test.mjs | 63 ++++++- 2 files changed, 149 insertions(+), 134 deletions(-) diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index 109bd62987c..aa796240e95 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -13,7 +13,7 @@ import { version } from '../data.mjs' import { __loadPatternDefaults } from './config.mjs' import cloneDeep from 'lodash.clonedeep' -const DISTANCE_DEBUG = false +const DISTANCE_DEBUG = true ////////////////////////////////////////////// // CONSTRUCTOR // @@ -74,11 +74,12 @@ export function Pattern(designConfig) { * @param {object} part - The part to add * @return {object} this - The Pattern instance */ -Pattern.prototype.addPart = function (part) { +Pattern.prototype.addPart = function (part, runtime = false) { if (typeof part?.draft === 'function') { if (part.name) { this.designConfig.parts.push(part) - this.__initialized = false + if (runtime) { + } else this.__initialized = false } else this.store.log.error(`Part must have a name`) } else this.store.log.error(`Part must have a draft() method`) @@ -370,6 +371,7 @@ Pattern.prototype.use = function (plugin, data) { */ Pattern.prototype.__addDependency = function (name, part, dep) { this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name]) + // #FIXME What's supposed to happen here? if (typeof this.__designParts[dep.name] === 'undefined') { this.config = this.__addPartConfig(this.__designParts[dep.name]) } @@ -417,12 +419,6 @@ Pattern.prototype.__addPartMeasurements = function (part, list = false) { } } } - if (part.from) this.__addPartMeasurements(part.from, list) - if (part.after) { - if (Array.isArray(part.after)) { - for (const dep of part.after) this.__addPartMeasurements(dep, list) - } else this.__addPartMeasurements(part.after, list) - } // Weed out duplicates this.config.measurements = [...new Set(list)] @@ -452,12 +448,6 @@ Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) } } } - if (part.from) this.__addPartOptionalMeasurements(part.from, list) - if (part.after) { - if (Array.isArray(part.after)) { - for (const dep of part.after) this.__addPartOptionalMeasurements(dep, list) - } else this.__addPartOptionalMeasurements(part.after, list) - } // Weed out duplicates if (list.length > 0) this.config.optionalMeasurements = [...new Set(list)] @@ -475,37 +465,27 @@ Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) Pattern.prototype.__addPartOptions = function (part) { if (!this.config.options) this.config.options = {} if (part.options) { + const partDistance = this.__mutated.partDistance?.[part.name] || 0 for (const optionName in part.options) { - if (!this.__mutated.optionDistance[optionName]) { - this.__mutated.optionDistance[optionName] = this.__mutated.partDistance?.[part.name] || 0 + const optionDistance = this.__mutated.optionDistance[optionName] + if (!optionDistance) { + this.__mutated.optionDistance[optionName] = partDistance // Keep design parts immutable in the pattern or risk subtle bugs this.config.options[optionName] = Object.freeze(part.options[optionName]) this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``) } else { if (DISTANCE_DEBUG) this.store.log.debug( - 'optionDistance for ' + - optionName + - ' is ' + - this.__mutated.optionDistance[optionName] + - ', and partDistance for ' + - part.name + - ' is ' + - this.__mutated.partDistance[part.name] + `optionDistance for ${optionName} is ${optionDistance} and partDistance for ${part.name} is ${partDistance}` ) - if (this.__mutated.optionDistance[optionName] > this.__mutated.partDistance[part.name]) { + if (optionDistance > partDistance) { this.config.options[optionName] = part.options[optionName] + this.__mutated.optionDistance[optionName] = partDistance this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``) } } } } - if (part.from) this.__addPartOptions(part.from) - if (part.after) { - if (Array.isArray(part.after)) { - for (const dep of part.after) this.__addPartOptions(dep) - } else this.__addPartOptions(part.after) - } return this } @@ -1272,6 +1252,76 @@ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDepende return this } +Pattern.prototype.__resolvePartMutation = function (part, dependency, depType) { + const current_part_distance = this.__mutated.partDistance[part.name] + const proposed_dependent_part_distance = current_part_distance + 1 + + this.__designParts[dependency.name] = Object.freeze(dependency) + switch (depType) { + case 'from': + this.__setFromHide(part, part.name, dependency.name) + this.__inject[part.name] = dependency.name + break + case 'after': + this.__setAfterHide(part, part.name, dependency.name) + this.__addDependency(part.name, part, dependency) + } + + if ( + typeof this.__mutated.partDistance[dependency.name] === 'undefined' || + this.__mutated.partDistance[dependency.name] < proposed_dependent_part_distance + ) { + this.__mutated.partDistance[dependency.name] = proposed_dependent_part_distance + if (DISTANCE_DEBUG) + this.store.log.debug( + `"${depType}:" partDistance for ${dependency.name} is ${ + this.__mutated.partDistance[dependency.name] + }` + ) + } +} + +Pattern.prototype.__resolvePart = function (part, distance = 0) { + if (distance === 0) { + this.__designParts[part.name] = Object.freeze(part) + } + let count = Object.keys(this.__designParts).length + distance++ + if (typeof this.__mutated.partDistance[part.name] === 'undefined') { + this.__mutated.partDistance[part.name] = distance + + if (DISTANCE_DEBUG) + this.store.log.debug( + `Base partDistance for ${part.name} is ${this.__mutated.partDistance[part.name]}` + ) + } + + // Hide when hideAll is set + if (part.hideAll) { + this.__mutated.partHide[part.name] = true + } + + // Resolve part mutations. first from then after + ;['from', 'after'].forEach((d) => { + if (part[d]) { + if (DISTANCE_DEBUG) this.store.log.debug(`Processing ${part.name} "${d}:"`) + + const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] + + depsOfType.forEach((dot) => { + this.__resolvePartMutation(part, dot, d) + const newCount = Object.keys(this.__designParts).length + if (count < newCount) { + this.__resolvePart(dot, distance) + count = newCount + } + }) + } + }) + + // add the part's config + this.__addPartConfig(part) +} /** * Resolves parts and their dependencies * @@ -1281,113 +1331,21 @@ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDepende * @return {Pattern} this - The Pattern instance */ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { - if (count === 0) { - for (const part of this.designConfig.parts) { - // Keep design parts immutable in the pattern or risk subtle bugs - this.__designParts[part.name] = Object.freeze(part) - } - } - distance++ - if (DISTANCE_DEBUG) this.store.log.debug('Distance incremented to ' + distance) for (const part of this.designConfig.parts) { - if (typeof this.__mutated.partDistance[part.name] === 'undefined') { - this.__mutated.partDistance[part.name] = distance - if (DISTANCE_DEBUG) - this.store.log.debug( - 'Base partDistance for ' + part.name + ' is ' + this.__mutated.partDistance[part.name] - ) - } - } - for (const [name, part] of Object.entries(this.__designParts)) { - const current_part_distance = this.__mutated.partDistance[part.name] - const proposed_dependent_part_distance = current_part_distance + 1 - // Hide when hideAll is set - if (part.hideAll) this.__mutated.partHide[part.name] = true - // Inject (from) - if (part.from) { - if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "from:"') - this.__setFromHide(part, name, part.from.name) - this.__designParts[part.from.name] = part.from - this.__inject[name] = part.from.name - if ( - typeof this.__mutated.partDistance[part.from.name] === 'undefined' || - this.__mutated.partDistance[part.from.name] < proposed_dependent_part_distance - ) { - this.__mutated.partDistance[part.from.name] = proposed_dependent_part_distance - if (DISTANCE_DEBUG) - this.store.log.debug( - '"from:" partDistance for ' + - part.from.name + - ' is ' + - this.__mutated.partDistance[part.from.name] - ) - } - } - // Simple dependency (after) - if (part.after) { - if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "after:"') - if (Array.isArray(part.after)) { - for (const dep of part.after) { - this.__setAfterHide(part, name, dep.name) - this.__designParts[dep.name] = dep - this.__addDependency(name, part, dep) - if ( - typeof this.__mutated.partDistance[dep.name] === 'undefined' || - this.__mutated.partDistance[dep.name] < proposed_dependent_part_distance - ) { - this.__mutated.partDistance[dep.name] = proposed_dependent_part_distance - if (DISTANCE_DEBUG) - this.store.log.debug( - '"after:" partDistance for ' + - dep.name + - ' is ' + - this.__mutated.partDistance[dep.name] - ) - } - } - } else { - this.__setAfterHide(part, name, part.after.name) - this.__designParts[part.after.name] = part.after - this.__addDependency(name, part, part.after) - if ( - typeof this.__mutated.partDistance[part.after.name] === 'undefined' || - this.__mutated.partDistance[part.after.name] < proposed_dependent_part_distance - ) { - this.__mutated.partDistance[part.after.name] = proposed_dependent_part_distance - if (DISTANCE_DEBUG) - this.store.log.debug( - '"after:" partDistance for ' + - part.after.name + - ' is ' + - this.__mutated.partDistance[part.after.name] - ) - } - } - } - } - // Did we discover any new dependencies? - const len = Object.keys(this.__designParts).length - // If so, resolve recursively - if (len > count) { - if (DISTANCE_DEBUG) this.store.log.debug('Recursing...') - return this.__resolveParts(len, distance) + this.__resolvePart(part, distance) } + // Print final part distances. for (const part of this.designConfig.parts) { - let qualifier = '' - if (DISTANCE_DEBUG) qualifier = 'final ' + let qualifier = DISTANCE_DEBUG ? 'final' : '' this.store.log.debug( - '⚪️ `' + - part.name + - '` ' + - qualifier + - 'options priority is __' + - this.__mutated.partDistance[part.name] + - '__' + `⚪️ ${part.name} ${qualifier} options priority is __${ + this.__mutated.partDistance[part.name] + }__` ) } - for (const part of Object.values(this.__designParts)) this.__addPartConfig(part) + // for (const part of Object.values(this.__designParts)) this.__addPartConfig(part) return this } diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index d3d5bcfc56f..bebc5f09b97 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -111,7 +111,7 @@ describe('Pattern', () => { parts: [partC], }) const pattern = new Pattern() - pattern.draft() + pattern.__init() it('Pattern.__init() should resolve all measurements', () => { expect( @@ -153,8 +153,8 @@ describe('Pattern', () => { }) it('Pattern.__init() should set config data in the store', () => { - expect(pattern.setStores[0].get('data.name')).to.equal('test') - expect(pattern.setStores[0].get('data.version')).to.equal('1.2.3') + expect(pattern.store.get('data.name')).to.equal('test') + expect(pattern.store.get('data.version')).to.equal('1.2.3') }) it('Pattern.__init() should resolve dependencies', () => { @@ -181,6 +181,63 @@ describe('Pattern', () => { expect(pattern.config.draftOrder[2]).to.equal('test.partC') }) + it('Pattern.__init() should overwrite options from dependencies', () => { + const partD = { + name: 'test.partD', + from: partB, + options: { + optB: { deg: 25, min: 15, max: 45 }, + }, + draft: ({ part }) => part, + } + + const Pattern = new Design({ + data: { + name: 'test', + version: '1.2.3', + }, + parts: [partD], + }) + const pattern = new Pattern() + pattern.__init() + for (const [key, value] of Object.entries(partD.options.optB)) { + expect(pattern.config.options.optB[key]).to.equal(value) + } + }) + + it('Pattern.__init() should overwrite options from complex dependencies', () => { + const partD = { + name: 'test.partD', + from: partB, + options: { + optB: { deg: 25, min: 15, max: 45 }, + }, + draft: ({ part }) => part, + } + + const partE = { + name: 'test.partE', + from: partD, + options: { + optB: { deg: 10, min: 15, max: 50 }, + }, + draft: ({ part }) => part, + } + + const Pattern = new Design({ + data: { + name: 'test', + version: '1.2.3', + }, + parts: [partC, partE], + }) + const pattern = new Pattern() + pattern.__init() + for (const [key, value] of Object.entries(partE.options.optB)) { + expect(pattern.config.options.optB[key]).to.equal(value) + } + }) + // I am aware this does too much for one unit test, but this is to simplify TDD // we can split it up later it('Pattern.__init() should resolve nested injections', () => { From 45e56cb6dd76333f40e887c50066430fb373b43d Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Mon, 20 Feb 2023 23:59:28 +0200 Subject: [PATCH 10/67] all part resolution business happens on a per-part basis --- packages/core/src/pattern.mjs | 244 ++++++++-------------- packages/core/tests/pattern-init.test.mjs | 8 +- 2 files changed, 91 insertions(+), 161 deletions(-) diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index aa796240e95..e5c7ff9e25a 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -13,8 +13,7 @@ import { version } from '../data.mjs' import { __loadPatternDefaults } from './config.mjs' import cloneDeep from 'lodash.clonedeep' -const DISTANCE_DEBUG = true - +const DISTANCE_DEBUG = false ////////////////////////////////////////////// // CONSTRUCTOR // ////////////////////////////////////////////// @@ -74,12 +73,11 @@ export function Pattern(designConfig) { * @param {object} part - The part to add * @return {object} this - The Pattern instance */ -Pattern.prototype.addPart = function (part, runtime = false) { +Pattern.prototype.addPart = function (part) { if (typeof part?.draft === 'function') { if (part.name) { this.designConfig.parts.push(part) - if (runtime) { - } else this.__initialized = false + this.__initialized = false } else this.store.log.error(`Part must have a name`) } else this.store.log.error(`Part must have a draft() method`) @@ -369,14 +367,12 @@ Pattern.prototype.use = function (plugin, data) { * @param {object} dep - The dependency configuration * @return {object} this - The Pattern instance */ -Pattern.prototype.__addDependency = function (name, part, dep) { - this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name]) - // #FIXME What's supposed to happen here? - if (typeof this.__designParts[dep.name] === 'undefined') { - this.config = this.__addPartConfig(this.__designParts[dep.name]) - } - - return this +Pattern.prototype.__addDependency = function (dependencyList, part, dep) { + this[dependencyList][part.name] = this[dependencyList][part.name] || [] + if (dependencyList == '__resolvedDependencies' && DISTANCE_DEBUG) + this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`) + if (this[dependencyList][part.name].indexOf(dep.name) === -1) + this[dependencyList][part.name].push(dep.name) } /** @@ -476,7 +472,7 @@ Pattern.prototype.__addPartOptions = function (part) { } else { if (DISTANCE_DEBUG) this.store.log.debug( - `optionDistance for ${optionName} is ${optionDistance} and partDistance for ${part.name} is ${partDistance}` + `optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__` ) if (optionDistance > partDistance) { this.config.options[optionName] = part.options[optionName] @@ -552,6 +548,11 @@ Pattern.prototype.__addPartPlugins = function (part) { `Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.` ) } + // swap from a conditional if needed + else if (plugins[name].condition) { + plugins[name] = plugin + this.store.log.info(`Plugin \`${name}\` was explicitly added. Changing from conditional.`) + } } } @@ -741,21 +742,11 @@ Pattern.prototype.__isPartHidden = function (partName) { Pattern.prototype.__isStackHidden = function (stackName) { if (!this.stacks[stackName]) return true const parts = this.stacks[stackName].getPartNames() - if (Array.isArray(this.settings[this.activeStack || 0].only)) { - for (const partName of parts) { - if (this.settings[this.activeStack || 0].only.includes(partName)) return false - } - return true - } for (const partName of parts) { - if (this.__designParts?.[partName]?.hide) return true - if (this.__designParts?.[partName]?.hideAll) return true - if (this.__mutated.partHide?.[partName]) return true - if (this.__mutated.partHideAll?.[partName]) return true - if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true + if (!this.__isPartHidden(partName)) return false } - return false + return true } /** @@ -1189,35 +1180,6 @@ Pattern.prototype.__pack = function () { return this } -/** - * Recursively solves part dependencies for a part - * - * @private - * @param {object} seen - Object to keep track of seen dependencies - * @param {string} part - Name of the part - * @param {object} graph - Dependency graph, used to call itself recursively - * @param {array} deps - List of dependencies - * @return {Array} deps - The list of dependencies - */ -Pattern.prototype.__resolveDependency = function ( - seen, - part, - graph = this.dependencies, - deps = [] -) { - if (typeof seen[part] === 'undefined') seen[part] = true - if (typeof graph[part] === 'string') graph[part] = [graph[part]] - if (Array.isArray(graph[part])) { - if (graph[part].length === 0) return [] - else { - if (deps.indexOf(graph[part]) === -1) deps.push(...graph[part]) - for (let apart of graph[part]) deps.concat(this.__resolveDependency(seen, apart, graph, deps)) - } - } - - return deps -} - /** * Resolves the draft order based on the configuation * @@ -1226,37 +1188,23 @@ Pattern.prototype.__resolveDependency = function ( * @return {Pattern} this - The Pattern instance */ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDependencies) { - let sorted = [] - let visited = {} - Object.keys(graph).forEach(function visit(name, ancestors) { - if (!Array.isArray(ancestors)) ancestors = [] - ancestors.push(name) - visited[name] = true - if (typeof graph[name] !== 'undefined') { - graph[name].forEach(function (dep) { - if (visited[dep]) return - visit(dep, ancestors.slice(0)) - }) - } - if (sorted.indexOf(name) < 0) sorted.push(name) - }) - - // Don't forget about parts without dependencies - for (const part in this.__designParts) { - if (sorted.indexOf(part) === -1) sorted.push(part) - } - + const sorted = Object.keys(this.__designParts).sort( + (p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1] + ) this.__draftOrder = sorted this.config.draftOrder = sorted return this } -Pattern.prototype.__resolvePartMutation = function (part, dependency, depType) { - const current_part_distance = this.__mutated.partDistance[part.name] - const proposed_dependent_part_distance = current_part_distance + 1 +Pattern.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) { + const part = depChain[0] this.__designParts[dependency.name] = Object.freeze(dependency) + this.__addDependency('__dependencies', part, dependency) + + depChain.forEach((c) => this.__addDependency('__resolvedDependencies', c, dependency)) + switch (depType) { case 'from': this.__setFromHide(part, part.name, dependency.name) @@ -1264,35 +1212,72 @@ Pattern.prototype.__resolvePartMutation = function (part, dependency, depType) { break case 'after': this.__setAfterHide(part, part.name, dependency.name) - this.__addDependency(part.name, part, dependency) - } - - if ( - typeof this.__mutated.partDistance[dependency.name] === 'undefined' || - this.__mutated.partDistance[dependency.name] < proposed_dependent_part_distance - ) { - this.__mutated.partDistance[dependency.name] = proposed_dependent_part_distance - if (DISTANCE_DEBUG) - this.store.log.debug( - `"${depType}:" partDistance for ${dependency.name} is ${ - this.__mutated.partDistance[dependency.name] - }` - ) } } -Pattern.prototype.__resolvePart = function (part, distance = 0) { +Pattern.prototype.__resolveMutatedPartDistance = function (partName) { + const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1 + let didChange = false + if (!this.__dependencies[partName]) return false + this.__dependencies[partName].forEach((dependency) => { + if ( + typeof this.__mutated.partDistance[dependency] === 'undefined' || + this.__mutated.partDistance[dependency] < proposed_dependent_part_distance + ) { + didChange = true + this.__mutated.partDistance[dependency] = proposed_dependent_part_distance + this.__resolveMutatedPartDistance(dependency) + } + if (DISTANCE_DEBUG) + this.store.log.debug( + `"${depType}:" partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` + ) + }) + + return didChange +} + +const depTypes = ['from', 'after'] +Pattern.prototype.__resolvePartDependencies = function (depChain, distance) { + // Resolve part Dependencies. first from then after + const part = depChain[0] + this.__resolvedDependencies[part.name] = this.__resolvedDependencies[part.name] || [] + + depTypes.forEach((d) => { + if (part[d]) { + if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`) + + const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] + + depsOfType.forEach((dot) => { + let count = Object.keys(this.__designParts).length + // if any changes resulted from resolving this part mutation + this.__resolvePartDependencyChain(depChain, dot, d) + // if a new part was added, resolve the part + const newCount = Object.keys(this.__designParts).length + if (count < newCount) { + this.__resolvePart([dot, ...depChain], distance) + count = newCount + } + }) + } + }) + + this.__resolveMutatedPartDistance(part.name) +} + +Pattern.prototype.__resolvePart = function (depChain, distance = 0) { + const part = depChain[0] if (distance === 0) { this.__designParts[part.name] = Object.freeze(part) } - let count = Object.keys(this.__designParts).length distance++ if (typeof this.__mutated.partDistance[part.name] === 'undefined') { this.__mutated.partDistance[part.name] = distance if (DISTANCE_DEBUG) this.store.log.debug( - `Base partDistance for ${part.name} is ${this.__mutated.partDistance[part.name]}` + `Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__` ) } @@ -1301,23 +1286,7 @@ Pattern.prototype.__resolvePart = function (part, distance = 0) { this.__mutated.partHide[part.name] = true } - // Resolve part mutations. first from then after - ;['from', 'after'].forEach((d) => { - if (part[d]) { - if (DISTANCE_DEBUG) this.store.log.debug(`Processing ${part.name} "${d}:"`) - - const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] - - depsOfType.forEach((dot) => { - this.__resolvePartMutation(part, dot, d) - const newCount = Object.keys(this.__designParts).length - if (count < newCount) { - this.__resolvePart(dot, distance) - count = newCount - } - }) - } - }) + this.__resolvePartDependencies(depChain, distance) // add the part's config this.__addPartConfig(part) @@ -1332,21 +1301,19 @@ Pattern.prototype.__resolvePart = function (part, distance = 0) { */ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { for (const part of this.designConfig.parts) { - this.__resolvePart(part, distance) + this.__resolvePart([part], distance) } // Print final part distances. for (const part of this.designConfig.parts) { let qualifier = DISTANCE_DEBUG ? 'final' : '' this.store.log.debug( - `⚪️ ${part.name} ${qualifier} options priority is __${ + `⚪️ \`${part.name}\` ${qualifier} options priority is __${ this.__mutated.partDistance[part.name] }__` ) } - // for (const part of Object.values(this.__designParts)) this.__addPartConfig(part) - return this } @@ -1354,35 +1321,10 @@ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { * Resolves parts depdendencies into a flat array * * @private - * @param {object} graph - The graph is used to call itsels recursively * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.__resolveDependencies = function (graph = false) { - if (!graph) graph = this.__dependencies - for (const i in this.__inject) { - const dependency = this.__inject[i] - if (typeof this.__dependencies[i] === 'undefined') this.__dependencies[i] = dependency - else if (this.__dependencies[i] !== dependency) { - if (typeof this.__dependencies[i] === 'string') { - this.__dependencies[i] = [this.__dependencies[i], dependency] - } else if (Array.isArray(this.__dependencies[i])) { - if (this.__dependencies[i].indexOf(dependency) === -1) - this.__dependencies[i].push(dependency) - } else { - this.store.log.error('Part dependencies should be a string or an array of strings') - throw new Error('Part dependencies should be a string or an array of strings') - } - } - } - - let resolved = {} - let seen = {} - for (let part in graph) resolved[part] = this.__resolveDependency(seen, part, graph) - for (let part in seen) if (typeof resolved[part] === 'undefined') resolved[part] = [] - - this.__resolvedDependencies = resolved - this.config.resolvedDependencies = resolved - +Pattern.prototype.__resolveDependencies = function () { + this.config.resolvedDependencies = this.__resolvedDependencies return this } @@ -1563,19 +1505,7 @@ Pattern.prototype.__wants = function (partName, set = 0) { */ function mergeDependencies(dep = [], current = []) { // Current dependencies - const list = [] - if (Array.isArray(current)) list.push(...current) - else if (typeof current === 'string') list.push(current) + const list = [].concat(current, dep) - if (Array.isArray(dep)) list.push(...dep) - else if (typeof dep === 'string') list.push(dep) - - // Dependencies should be parts names (string) not the object - const deps = [] - for (const part of [...new Set(list)]) { - if (typeof part === 'object') deps.push(part.name) - else deps.push(part) - } - - return deps + return [...new Set(list)] } diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index bebc5f09b97..8f5c76621d7 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -324,10 +324,10 @@ describe('Pattern', () => { expect(pattern.config.options.optionR.list[1]).to.equal('green') expect(pattern.config.options.optionR.list[2]).to.equal('blue') // Dependencies - expect(pattern.__dependencies.partB[0]).to.equal('partA') - expect(pattern.__dependencies.partC[0]).to.equal('partB') - expect(pattern.__dependencies.partR[0]).to.equal('partC') - expect(pattern.__dependencies.partR[1]).to.equal('partA') + expect(pattern.__dependencies.partB).to.include('partA') + expect(pattern.__dependencies.partC).to.include('partB') + expect(pattern.__dependencies.partR).to.include('partC') + expect(pattern.__dependencies.partR).to.include('partA') // Inject expect(pattern.__inject.partB).to.equal('partA') expect(pattern.__inject.partC).to.equal('partB') From 58a02f79f5123e037a441ca6c155b22cfc50ef1e Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Tue, 21 Feb 2023 16:17:57 +0200 Subject: [PATCH 11/67] add tests for runtime part adding --- packages/core/src/pattern.mjs | 7 +- .../core/tests/pattern-runtime-parts.test.mjs | 101 ++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/core/tests/pattern-runtime-parts.test.mjs diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index e5c7ff9e25a..f9abd74be97 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -73,11 +73,14 @@ export function Pattern(designConfig) { * @param {object} part - The part to add * @return {object} this - The Pattern instance */ -Pattern.prototype.addPart = function (part) { +Pattern.prototype.addPart = function (part, resolveImmediately = false) { if (typeof part?.draft === 'function') { if (part.name) { this.designConfig.parts.push(part) - this.__initialized = false + if (resolveImmediately) { + this.store.log.debug(`Perfoming runtime resolution of new part ${part.name}`) + this.__resolvePart([part]) + } else this.__initialized = false } else this.store.log.error(`Part must have a name`) } else this.store.log.error(`Part must have a draft() method`) diff --git a/packages/core/tests/pattern-runtime-parts.test.mjs b/packages/core/tests/pattern-runtime-parts.test.mjs new file mode 100644 index 00000000000..a3a640e6721 --- /dev/null +++ b/packages/core/tests/pattern-runtime-parts.test.mjs @@ -0,0 +1,101 @@ +import chai from 'chai' +import { Design } from '../src/index.mjs' + +const expect = chai.expect + +describe('Pattern', () => { + describe('.addPart()', () => { + const part1 = { + name: 'test', + draft: ({ part }) => part, + } + + const part2 = { + name: 'test2', + from: part1, + draft: ({ part }) => part, + } + + const part3 = { + name: 'test3', + after: part2, + draft: ({ part }) => part, + } + + describe('with resolveImmediately: true', () => { + it('Should add the part to the internal part object', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + pattern.addPart(part2, true) + expect(pattern.__designParts.test2).to.equal(part2) + }) + + it('Should resolve injected dependencies for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + pattern.addPart(part2, true) + expect(pattern.__inject.test2).to.equal('test') + }) + + it('Should resolve all dependencies for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + pattern.addPart(part3, true) + expect(pattern.config.resolvedDependencies.test3).to.have.members(['test', 'test2']) + expect(pattern.__designParts.test2).to.equal(part2) + }) + + it('Should add a the measurements for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const part2 = { + name: 'test2', + measurements: ['neck'], + draft: ({ part }) => part, + } + + pattern.addPart(part2, true) + expect(pattern.config.measurements).to.include('neck') + }) + + it('Should add the plugins for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const plugin = { name: 'testPlugin' } + const part2 = { + name: 'test2', + plugins: [plugin], + draft: ({ part }) => part, + } + + pattern.addPart(part2, true) + expect(pattern.config.plugins.testPlugin).to.equal(plugin) + }) + + it('Should add the options for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const opt1 = { pct: 10, min: 0, max: 50 } + const part2 = { + name: 'test2', + options: { + opt1, + }, + draft: ({ part }) => part, + } + + pattern.addPart(part2, true) + expect(pattern.config.options.opt1).to.equal(opt1) + }) + }) + }) +}) From 46f539df11686113bd137f0740e064ad34f7e6a4 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Tue, 21 Feb 2023 22:33:57 +0200 Subject: [PATCH 12/67] move all pattern config resolution to separate class --- packages/core/src/pattern.mjs | 502 ++---------------- packages/core/src/patternConfig.mjs | 419 +++++++++++++++ packages/core/tests/pattern-init.test.mjs | 38 +- .../core/tests/pattern-runtime-parts.test.mjs | 71 ++- packages/core/tests/snap.test.mjs | 3 + 5 files changed, 534 insertions(+), 499 deletions(-) create mode 100644 packages/core/src/patternConfig.mjs diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index f9abd74be97..d593c360304 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -11,9 +11,9 @@ import { Store } from './store.mjs' import { Hooks } from './hooks.mjs' import { version } from '../data.mjs' import { __loadPatternDefaults } from './config.mjs' +import { PatternConfig, getPluginName } from './patternConfig.mjs' import cloneDeep from 'lodash.clonedeep' -const DISTANCE_DEBUG = false ////////////////////////////////////////////// // CONSTRUCTOR // ////////////////////////////////////////////// @@ -25,7 +25,7 @@ const DISTANCE_DEBUG = false * @param {object} config - The Design config * @return {object} this - The Pattern instance */ -export function Pattern(designConfig) { +export function Pattern(designConfig = {}) { // Non-enumerable properties __addNonEnumProp(this, 'plugins', {}) __addNonEnumProp(this, 'width', 0) @@ -39,19 +39,9 @@ export function Pattern(designConfig) { __addNonEnumProp(this, 'Attributes', Attributes) __addNonEnumProp(this, 'macros', {}) __addNonEnumProp(this, '__initialized', false) - __addNonEnumProp(this, '__designParts', {}) - __addNonEnumProp(this, '__inject', {}) - __addNonEnumProp(this, '__dependencies', {}) - __addNonEnumProp(this, '__resolvedDependencies', {}) - __addNonEnumProp(this, '__resolvedParts', []) + __addNonEnumProp(this, 'config.parts', {}) + __addNonEnumProp(this, 'config.resolvedDependencies', {}) __addNonEnumProp(this, '__storeMethods', new Set()) - __addNonEnumProp(this, '__mutated', { - optionDistance: {}, - partDistance: {}, - partHide: {}, - partHideAll: {}, - }) - __addNonEnumProp(this, '__draftOrder', []) __addNonEnumProp(this, '__hide', {}) // Enumerable properties @@ -59,6 +49,7 @@ export function Pattern(designConfig) { this.config = {} // Will hold the resolved pattern after calling __init() this.store = new Store() // Pattern-wide store this.setStores = [] // Per-set stores + __addNonEnumProp(this, '__configResolver', new PatternConfig(this)) // handles config resolution during __init() as well as runtime part adding return this } @@ -74,16 +65,11 @@ export function Pattern(designConfig) { * @return {object} this - The Pattern instance */ Pattern.prototype.addPart = function (part, resolveImmediately = false) { - if (typeof part?.draft === 'function') { - if (part.name) { - this.designConfig.parts.push(part) - if (resolveImmediately) { - this.store.log.debug(`Perfoming runtime resolution of new part ${part.name}`) - this.__resolvePart([part]) - } else this.__initialized = false - } else this.store.log.error(`Part must have a name`) - } else this.store.log.error(`Part must have a draft() method`) - + if (this.__configResolver.validatePart(part) && this.designConfig.parts.indexOf(part) === -1) { + this.designConfig.parts.push(part) + if (resolveImmediately) this.__configResolver.addPart(part) + else this.__initialized = false + } return this } @@ -132,15 +118,15 @@ Pattern.prototype.createPartForSet = function (partName, set = 0) { this.parts[set][partName] = this.__createPartWithContext(partName, set) // Handle inject/inheritance - if (typeof this.__inject[partName] === 'string') { + if (typeof this.config.inject[partName] === 'string') { this.setStores[set].log.debug( - `Creating part \`${partName}\` from part \`${this.__inject[partName]}\`` + `Creating part \`${partName}\` from part \`${this.config.inject[partName]}\`` ) try { - this.parts[set][partName].__inject(this.parts[set][this.__inject[partName]]) + this.parts[set][partName].__inject(this.parts[set][this.config.inject[partName]]) } catch (err) { this.setStores[set].log.error([ - `Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``, + `Could not inject part \`${this.config.inject[partName]}\` into part \`${partName}\``, err, ]) } @@ -161,11 +147,11 @@ Pattern.prototype.createPartForSet = function (partName, set = 0) { } Pattern.prototype.draftPartForSet = function (partName, set) { - if (typeof this.__designParts?.[partName]?.draft === 'function') { + if (typeof this.config.parts?.[partName]?.draft === 'function') { this.activePart = partName try { this.__runHooks('prePartDraft') - const result = this.__designParts[partName].draft(this.parts[set][partName].shorthand()) + const result = this.config.parts[partName].draft(this.parts[set][partName].shorthand()) this.__runHooks('postPartDraft') if (typeof result === 'undefined') { this.setStores[set].log.error( @@ -361,209 +347,6 @@ Pattern.prototype.use = function (plugin, data) { // PRIVATE METHODS // ////////////////////////////////////////////// -/** - * Adds a part as a simple dependency - * - * @private - * @param {string} name - The name of the dependency - * @param {object} part - The part configuration - * @param {object} dep - The dependency configuration - * @return {object} this - The Pattern instance - */ -Pattern.prototype.__addDependency = function (dependencyList, part, dep) { - this[dependencyList][part.name] = this[dependencyList][part.name] || [] - if (dependencyList == '__resolvedDependencies' && DISTANCE_DEBUG) - this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`) - if (this[dependencyList][part.name].indexOf(dep.name) === -1) - this[dependencyList][part.name].push(dep.name) -} - -/** - * Resolves/Adds a part's design configuration to the pattern config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @param {onject} config - The global config - * @param {Store} store - The store, used for logging - * @return {object} config - The mutated global config - */ -Pattern.prototype.__addPartConfig = function (part) { - if (this.__resolvedParts.includes(part.name)) return this - - // Add parts, using set to keep them unique in the array - this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)] - - return this.__addPartOptions(part) - .__addPartMeasurements(part) - .__addPartOptionalMeasurements(part) - .__addPartPlugins(part) -} - -/** - * Resolves/Adds a part's configured measurements to the global config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @param {array} list - The list of resolved measurements - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__addPartMeasurements = function (part, list = false) { - if (!this.config.measurements) this.config.measurements = [] - if (!list) list = this.config.measurements - if (part.measurements) { - for (const m of part.measurements) { - if (list.indexOf(m) === -1) { - list.push(m) - this.store.log.debug(`🟠 __${m}__ measurement is required in \`${part.name}\``) - } - } - } - - // Weed out duplicates - this.config.measurements = [...new Set(list)] - - return this -} - -/** - * Resolves/Adds a part's configured optional measurements to the global config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @param {array} list - The list of resolved optional measurements - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) { - if (!this.config.optionalMeasurements) this.config.optionalMeasurements = [] - if (!list) list = this.config.optionalMeasurements - if (part.optionalMeasurements) { - for (const m of part.optionalMeasurements) { - // Don't add it's a required measurement for another part - if (this.config.measurements.indexOf(m) === -1) { - if (list.indexOf(m) === -1) { - list.push(m) - this.store.log.debug(`🟡 __${m}__ measurement is optional in \`${part.name}\``) - } - } - } - } - - // Weed out duplicates - if (list.length > 0) this.config.optionalMeasurements = [...new Set(list)] - - return this -} - -/** - * Resolves/Adds a part's configured options to the global config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__addPartOptions = function (part) { - if (!this.config.options) this.config.options = {} - if (part.options) { - const partDistance = this.__mutated.partDistance?.[part.name] || 0 - for (const optionName in part.options) { - const optionDistance = this.__mutated.optionDistance[optionName] - if (!optionDistance) { - this.__mutated.optionDistance[optionName] = partDistance - // Keep design parts immutable in the pattern or risk subtle bugs - this.config.options[optionName] = Object.freeze(part.options[optionName]) - this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``) - } else { - if (DISTANCE_DEBUG) - this.store.log.debug( - `optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__` - ) - if (optionDistance > partDistance) { - this.config.options[optionName] = part.options[optionName] - this.__mutated.optionDistance[optionName] = partDistance - this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``) - } - } - } - } - - return this -} - -function getPluginName(plugin) { - if (Array.isArray(plugin)) { - if (plugin[0].name) return plugin[0].name - if (plugin[0].plugin.name) return plugin[0].plugin.name - } else { - if (plugin.name) return plugin.name - if (plugin.plugin?.name) return plugin.plugin.name - } - - return false -} - -/** - * Resolves/Adds a part's configured plugins to the global config - * - * @private - * @param {Part} part - The part of which to resolve the config - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__addPartPlugins = function (part) { - if (!part.plugins) return this - if (!this.config.plugins) this.config.plugins = {} - const plugins = { ...this.config.plugins } - // Side-step immutability of the part object to ensure plugins is an array - let partPlugins = part.plugins - if (!Array.isArray(partPlugins)) partPlugins = [partPlugins] - // Go through list of part plugins - for (let plugin of partPlugins) { - const name = getPluginName(plugin) - this.store.log.debug( - plugin.plugin - ? `🔌 Resolved __${name}__ conditional plugin in \`${part.name}\`` - : `🔌 Resolved __${name}__ plugin in \`${part.name}\`` - ) - // Handle [plugin, data] scenario - if (Array.isArray(plugin)) { - const pluginObj = { ...plugin[0], data: plugin[1] } - plugin = pluginObj - } - if (!plugins[name]) { - // New plugin, so we load it - plugins[name] = plugin - this.store.log.info( - plugin.condition - ? `New plugin conditionally added: \`${name}\`` - : `New plugin added: \`${name}\`` - ) - } else { - // Existing plugin, takes some more work - if (plugin.plugin && plugin.condition) { - // Multiple instances of the same plugin with different conditions - // will all be added, so we need to change the name. - if (plugins[name]?.condition) { - plugins[name + '_'] = plugin - this.store.log.info( - `Plugin \`${name}\` was conditionally added again. Renaming to ${name}_.` - ) - } else - this.store.log.info( - `Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.` - ) - } - // swap from a conditional if needed - else if (plugins[name].condition) { - plugins[name] = plugin - this.store.log.info(`Plugin \`${name}\` was explicitly added. Changing from conditional.`) - } - } - } - - this.config.plugins = { ...plugins } - - return this -} - /** * Creates a store for a set (of settings) * @@ -620,7 +403,7 @@ Pattern.prototype.__createPartWithContext = function (name, set) { const part = new Part() part.name = name part.set = set - part.stack = this.__designParts[name]?.stack || name + part.stack = this.config.parts[name]?.stack || name part.context = { parts: this.parts[set], config: this.config, @@ -700,14 +483,11 @@ Pattern.prototype.__init = function () { * This methods does that, and resolves the design config + user settings */ this.__resolveParts() // Resolves parts - .__resolveDependencies() // Resolves dependencies - .__resolveDraftOrder() // Resolves draft order + .__resolveConfig() // Gets the config from the resolver .__loadPlugins() // Loads plugins .__loadConfigData() // Makes config data available in store - .__filterOptionalMeasurements() // Removes required m's from optional list - .__loadOptionDefaults() // Merges default options with user provided ones - this.store.log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`) + this.store.log.info(`Pattern initialized. Draft order is: ${this.config.draftOrder.join(', ')}`) this.__runHooks('postInit') this.__initialized = true @@ -726,10 +506,10 @@ Pattern.prototype.__isPartHidden = function (partName) { if (Array.isArray(this.settings[this.activeSet || 0].only)) { if (this.settings[this.activeSet || 0].only.includes(partName)) return false } - if (this.__designParts?.[partName]?.hide) return true - if (this.__designParts?.[partName]?.hideAll) return true - if (this.__mutated.partHide?.[partName]) return true - if (this.__mutated.partHideAll?.[partName]) return true + if (this.config.parts?.[partName]?.hide) return true + if (this.config.parts?.[partName]?.hideAll) return true + if (this.config.partHide?.[partName]) return true + if (this.config.partHideAll?.[partName]) return true if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true return false @@ -822,40 +602,6 @@ Pattern.prototype.__loadConfigData = function () { return this } -/** - * Merges defaults for options with user-provided options - * - * @private - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__loadOptionDefaults = function () { - if (!this.config.options) this.config.options = {} - if (Object.keys(this.config.options).length < 1) return this - for (const i in this.settings) { - for (const [name, option] of Object.entries(this.config.options)) { - // Don't overwrite user-provided settings.options - if (typeof this.settings[i].options[name] === 'undefined') { - if (typeof option === 'object') { - if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100 - else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm - else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg - else if (typeof option.count !== 'undefined') - this.settings[i].options[name] = option.count - else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool - else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt - else { - let err = 'Unknown option type: ' + JSON.stringify(option) - this.store.log.error(err) - throw new Error(err) - } - } else this.settings[i].options[name] = option - } - } - } - - return this -} - /** * Loads a plugin * @@ -1049,8 +795,8 @@ Pattern.prototype.__needs = function (partName, set = 0) { // Walk the only parts, checking each one for a match in its dependencies for (const part of only) { if (part === partName) return true - if (this.__resolvedDependencies[part]) { - for (const dependency of this.__resolvedDependencies[part]) { + if (this.config.resolvedDependencies[part]) { + for (const dependency of this.config.resolvedDependencies[part]) { if (dependency === partName) return true } } @@ -1183,117 +929,11 @@ Pattern.prototype.__pack = function () { return this } -/** - * Resolves the draft order based on the configuation - * - * @private - * @param {object} graph - The object of resolved dependencies, used to call itself recursively - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDependencies) { - const sorted = Object.keys(this.__designParts).sort( - (p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1] - ) - this.__draftOrder = sorted - this.config.draftOrder = sorted - +Pattern.prototype.__resolveConfig = function () { + this.config = this.__configResolver.asConfig() return this } -Pattern.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) { - const part = depChain[0] - - this.__designParts[dependency.name] = Object.freeze(dependency) - this.__addDependency('__dependencies', part, dependency) - - depChain.forEach((c) => this.__addDependency('__resolvedDependencies', c, dependency)) - - switch (depType) { - case 'from': - this.__setFromHide(part, part.name, dependency.name) - this.__inject[part.name] = dependency.name - break - case 'after': - this.__setAfterHide(part, part.name, dependency.name) - } -} - -Pattern.prototype.__resolveMutatedPartDistance = function (partName) { - const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1 - let didChange = false - if (!this.__dependencies[partName]) return false - this.__dependencies[partName].forEach((dependency) => { - if ( - typeof this.__mutated.partDistance[dependency] === 'undefined' || - this.__mutated.partDistance[dependency] < proposed_dependent_part_distance - ) { - didChange = true - this.__mutated.partDistance[dependency] = proposed_dependent_part_distance - this.__resolveMutatedPartDistance(dependency) - } - if (DISTANCE_DEBUG) - this.store.log.debug( - `"${depType}:" partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` - ) - }) - - return didChange -} - -const depTypes = ['from', 'after'] -Pattern.prototype.__resolvePartDependencies = function (depChain, distance) { - // Resolve part Dependencies. first from then after - const part = depChain[0] - this.__resolvedDependencies[part.name] = this.__resolvedDependencies[part.name] || [] - - depTypes.forEach((d) => { - if (part[d]) { - if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`) - - const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] - - depsOfType.forEach((dot) => { - let count = Object.keys(this.__designParts).length - // if any changes resulted from resolving this part mutation - this.__resolvePartDependencyChain(depChain, dot, d) - // if a new part was added, resolve the part - const newCount = Object.keys(this.__designParts).length - if (count < newCount) { - this.__resolvePart([dot, ...depChain], distance) - count = newCount - } - }) - } - }) - - this.__resolveMutatedPartDistance(part.name) -} - -Pattern.prototype.__resolvePart = function (depChain, distance = 0) { - const part = depChain[0] - if (distance === 0) { - this.__designParts[part.name] = Object.freeze(part) - } - distance++ - if (typeof this.__mutated.partDistance[part.name] === 'undefined') { - this.__mutated.partDistance[part.name] = distance - - if (DISTANCE_DEBUG) - this.store.log.debug( - `Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__` - ) - } - - // Hide when hideAll is set - if (part.hideAll) { - this.__mutated.partHide[part.name] = true - } - - this.__resolvePartDependencies(depChain, distance) - - // add the part's config - this.__addPartConfig(part) -} /** * Resolves parts and their dependencies * @@ -1302,35 +942,15 @@ Pattern.prototype.__resolvePart = function (depChain, distance = 0) { * @param {int} distance - Keeps track of how far the dependency is from the pattern * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { - for (const part of this.designConfig.parts) { - this.__resolvePart([part], distance) - } +Pattern.prototype.__resolveParts = function () { + this.designConfig.parts.forEach((p) => this.__configResolver.addPart(p)) // Print final part distances. - for (const part of this.designConfig.parts) { - let qualifier = DISTANCE_DEBUG ? 'final' : '' - this.store.log.debug( - `⚪️ \`${part.name}\` ${qualifier} options priority is __${ - this.__mutated.partDistance[part.name] - }__` - ) - } + this.__configResolver.logPartDistances() return this } -/** - * Resolves parts depdendencies into a flat array - * - * @private - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__resolveDependencies = function () { - this.config.resolvedDependencies = this.__resolvedDependencies - return this -} - /** * Runs subscriptions to a given lifecycle hook * @@ -1364,49 +984,6 @@ Pattern.prototype.__setBase = function () { } } -/** - * Sets visibility of a dependency based on its config - * - * @private - * @param {Part} part - The part of which this is a dependency - * @param {string} name - The name of the part - * @param {string} depName - The name of the dependency - * @param {int} set - The index of the set in the list of settings - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__setFromHide = function (part, name, depName) { - if ( - part.hideDependencies || - part.hideAll || - this.__mutated.partHide[name] || - this.__mutated.partHideAll[name] - ) { - this.__mutated.partHide[depName] = true - this.__mutated.partHideAll[depName] = true - } - - return this -} - -/** - * Sets visibility of an 'after' dependency based on its config - * - * @private - * @param {Part} part - The part of which this is a dependency - * @param {string} name - The name of the part - * @param {string} depName - The name of the dependency - * @param {int} set - The index of the set in the list of settings - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__setAfterHide = function (part, name, depName) { - if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) { - this.__mutated.partHide[depName] = true - this.__mutated.partHideAll[depName] = true - } - - return this -} - /** * Returns the absolute value of a snapped percentage option * @@ -1493,22 +1070,3 @@ Pattern.prototype.__wants = function (partName, set = 0) { return true } - -////////////////////////////////////////////// -// STATIC PRIVATE FUNCTIONS // -////////////////////////////////////////////// - -/** - * Merges dependencies into a flat list - * - * @private - * @param {array} dep - New dependencies - * @param {array} current - Current dependencies - * @return {array} deps - Merged dependencies - */ -function mergeDependencies(dep = [], current = []) { - // Current dependencies - const list = [].concat(current, dep) - - return [...new Set(list)] -} diff --git a/packages/core/src/patternConfig.mjs b/packages/core/src/patternConfig.mjs new file mode 100644 index 00000000000..a28abb1b432 --- /dev/null +++ b/packages/core/src/patternConfig.mjs @@ -0,0 +1,419 @@ +import { __addNonEnumProp } from './utils.mjs' + +export function getPluginName(plugin) { + if (Array.isArray(plugin)) { + if (plugin[0].name) return plugin[0].name + if (plugin[0].plugin.name) return plugin[0].plugin.name + } else { + if (plugin.name) return plugin.name + if (plugin.plugin?.name) return plugin.plugin.name + } + + return false +} + +export function PatternConfig(pattern) { + this.pattern = pattern + this.store = pattern.store + __addNonEnumProp(this, 'plugins', { ...(pattern.designConfig.plugins || {}) }) + __addNonEnumProp(this, 'options', { ...(pattern.designConfig.options || {}) }) + __addNonEnumProp(this, 'measurements', [...(pattern.designConfig.measurements || [])]) + __addNonEnumProp(this, 'optionalMeasurements', [ + ...(pattern.designConfig.optionalMeasurements || []), + ]) + __addNonEnumProp(this, 'inject', {}) + __addNonEnumProp(this, 'directDependencies', {}) + __addNonEnumProp(this, 'resolvedDependencies', {}) + __addNonEnumProp(this, 'parts', {}) + __addNonEnumProp(this, '__resolvedParts', {}) + __addNonEnumProp(this, '__mutated', { + optionDistance: {}, + partDistance: {}, + partHide: {}, + partHideAll: {}, + }) +} + +const DISTANCE_DEBUG = false + +PatternConfig.prototype.validatePart = function (part) { + if (typeof part?.draft !== 'function') { + this.store.log.error(`Part must have a draft() method`) + return false + } + + if (!part.name) { + this.store.log.error(`Part must have a name`) + return false + } + + return true +} +PatternConfig.prototype.addPart = function (part) { + if (this.validatePart(part)) this.__resolvePart([part]) + + return this +} + +PatternConfig.prototype.logPartDistances = function () { + for (const partName in this.parts) { + let qualifier = DISTANCE_DEBUG ? 'final' : '' + this.store.log.debug( + `⚪️ \`${partName}\` ${qualifier} options priority is __${this.__mutated.partDistance[partName]}__` + ) + } +} + +PatternConfig.prototype.asConfig = function () { + return { + parts: this.parts, + plugins: this.plugins, + measurements: this.measurements, + options: this.options, + optionalMeasurements: this.optionalMeasurements, + resolvedDependencies: this.resolvedDependencies, + directDependencies: this.directDependencies, + inject: this.inject, + draftOrder: this.__resolveDraftOrder(), + partHide: this.__mutated.partHide, + partHideAll: this.__mutated.partHideAll, + } +} + +PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) { + const part = depChain[0] + if (distance === 0) { + this.parts[part.name] = Object.freeze(part) + } + distance++ + if (typeof this.__mutated.partDistance[part.name] === 'undefined') { + this.__mutated.partDistance[part.name] = distance + + if (DISTANCE_DEBUG) + this.store.log.debug( + `Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__` + ) + } + + // Hide when hideAll is set + if (part.hideAll) { + this.__mutated.partHide[part.name] = true + } + + this.__resolvePartDependencies(depChain, distance) + + // add the part's config + this.__addPartConfig(part) +} + +/** + * Resolves/Adds a part's design configuration to the pattern config + * + * @private + * @param {Part} part - The part of which to resolve the config + * @param {onject} config - The global config + * @param {Store} store - The store, used for logging + * @return {object} config - The mutated global config + */ +PatternConfig.prototype.__addPartConfig = function (part) { + if (this.__resolvedParts[part.name]) return this + + // Add parts, using set to keep them unique in the array + // this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)] + + return this.__addPartOptions(part) + .__addPartMeasurements(part, true) + .__addPartMeasurements(part, false) + .__addPartPlugins(part) +} + +/** + * Resolves/Adds a part's configured options to the global config + * + * @private + * @param {Part} part - The part of which to resolve the config + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__addPartOptions = function (part) { + if (!part.options) return this + + const partDistance = this.__mutated.partDistance?.[part.name] || 0 + for (const optionName in part.options) { + const option = part.options[optionName] + const optionDistance = this.__mutated.optionDistance[optionName] + if (optionDistance && DISTANCE_DEBUG) + this.store.log.debug( + `optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__` + ) + if (!optionDistance || optionDistance > partDistance) { + this.__mutated.optionDistance[optionName] = partDistance + // Keep design parts immutable in the pattern or risk subtle bugs + this.options[optionName] = Object.freeze(option) + this.store.log.debug( + optionDistance + ? `🟣 __${optionName}__ option overwritten by \`${part.name}\`` + : `🔵 __${optionName}__ option loaded from part \`${part.name}\`` + ) + this.__loadOptionDefault(optionName, option) + } + } + + return this +} + +/** + * Resolves/Adds a part's configured measurements to the global config + * + * @private + * @param {Part} part - The part of which to resolve the config + * @param {array} list - The list of resolved measurements + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__addPartMeasurements = function (part, optional = false) { + const listType = optional ? 'optionalMeasurements' : 'measurements' + if (part[listType]) { + part[listType].forEach((m) => { + const isInReqList = this.measurements.indexOf(m) !== -1 + const optInd = this.optionalMeasurements.indexOf(m) + const isInOptList = optInd !== -1 + + if (isInReqList) return + if (optional && !isInOptList) this.optionalMeasurements.push(m) + if (!optional) { + this.measurements.push(m) + + if (isInOptList) this.optionalMeasurements.splice(optInd, 1) + } + + this.store.log.debug( + `🟠 __${m}__ measurement is ${optional ? 'optional' : 'required'} in \`${part.name}\`` + ) + }) + } + + return this +} + +/** + * Resolves/Adds a part's configured plugins to the global config + * + * @private + * @param {Part} part - The part of which to resolve the config + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__addPartPlugins = function (part) { + if (!part.plugins) return this + + const plugins = this.plugins + // Side-step immutability of the part object to ensure plugins is an array + let partPlugins = part.plugins + if (!Array.isArray(partPlugins)) partPlugins = [partPlugins] + // Go through list of part plugins + for (let plugin of partPlugins) { + const name = getPluginName(plugin) + this.store.log.debug( + plugin.plugin + ? `🔌 Resolved __${name}__ conditional plugin in \`${part.name}\`` + : `🔌 Resolved __${name}__ plugin in \`${part.name}\`` + ) + // Handle [plugin, data] scenario + if (Array.isArray(plugin)) { + const pluginObj = { ...plugin[0], data: plugin[1] } + plugin = pluginObj + } + if (!plugins[name]) { + // New plugin, so we load it + plugins[name] = plugin + this.store.log.info( + plugin.condition + ? `New plugin conditionally added: \`${name}\`` + : `New plugin added: \`${name}\`` + ) + } else { + // Existing plugin, takes some more work + if (plugin.plugin && plugin.condition) { + // Multiple instances of the same plugin with different conditions + // will all be added, so we need to change the name. + if (plugins[name]?.condition) { + plugins[name + '_'] = plugin + this.store.log.info( + `Plugin \`${name}\` was conditionally added again. Renaming to ${name}_.` + ) + } else + this.store.log.info( + `Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.` + ) + } + // swap from a conditional if needed + else if (plugins[name].condition) { + plugins[name] = plugin + this.store.log.info(`Plugin \`${name}\` was explicitly added. Changing from conditional.`) + } + } + } + + return this +} + +PatternConfig.prototype.__loadOptionDefault = function (optionName, option) { + this.pattern.settings.forEach((set) => { + if (typeof set.options[optionName] !== 'undefined') return + if (typeof option === 'object') { + if (typeof option.pct !== 'undefined') set.options[optionName] = option.pct / 100 + else if (typeof option.mm !== 'undefined') set.options[optionName] = option.mm + else if (typeof option.deg !== 'undefined') set.options[optionName] = option.deg + else if (typeof option.count !== 'undefined') set.options[optionName] = option.count + else if (typeof option.bool !== 'undefined') set.options[optionName] = option.bool + else if (typeof option.dflt !== 'undefined') set.options[optionName] = option.dflt + else { + let err = 'Unknown option type: ' + JSON.stringify(option) + this.store.log.error(err) + throw new Error(err) + } + } else set.options[optionName] = option + }) +} + +PatternConfig.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) { + const part = depChain[0] + + this.parts[dependency.name] = Object.freeze(dependency) + this.__addDependency('directDependencies', part, dependency) + + depChain.forEach((c) => this.__addDependency('resolvedDependencies', c, dependency)) + + switch (depType) { + case 'from': + this.__setFromHide(part, part.name, dependency.name) + this.inject[part.name] = dependency.name + break + case 'after': + this.__setAfterHide(part, part.name, dependency.name) + } +} + +PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) { + const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1 + let didChange = false + if (!this.directDependencies[partName]) return false + this.directDependencies[partName].forEach((dependency) => { + if ( + typeof this.__mutated.partDistance[dependency] === 'undefined' || + this.__mutated.partDistance[dependency] < proposed_dependent_part_distance + ) { + didChange = true + this.__mutated.partDistance[dependency] = proposed_dependent_part_distance + this.__resolveMutatedPartDistance(dependency) + } + if (DISTANCE_DEBUG) + this.store.log.debug( + `partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` + ) + }) + + return didChange +} + +const depTypes = ['from', 'after'] +PatternConfig.prototype.__resolvePartDependencies = function (depChain, distance) { + // Resolve part Dependencies. first from then after + const part = depChain[0] + this.resolvedDependencies[part.name] = this.resolvedDependencies[part.name] || [] + + depTypes.forEach((d) => { + if (part[d]) { + if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`) + + const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] + + depsOfType.forEach((dot) => { + let count = Object.keys(this.parts).length + // if any changes resulted from resolving this part mutation + this.__resolvePartDependencyChain(depChain, dot, d) + // if a new part was added, resolve the part + const newCount = Object.keys(this.parts).length + if (count < newCount) { + this.__resolvePart([dot, ...depChain], distance) + count = newCount + } + }) + } + }) + + this.__resolveMutatedPartDistance(part.name) +} + +/** + * Adds a part as a simple dependency + * + * @private + * @param {string} name - The name of the dependency + * @param {object} part - The part configuration + * @param {object} dep - The dependency configuration + * @return {object} this - The Pattern instance + */ +PatternConfig.prototype.__addDependency = function (dependencyList, part, dep) { + this[dependencyList][part.name] = this[dependencyList][part.name] || [] + if (dependencyList == 'resolvedDependencies' && DISTANCE_DEBUG) + this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`) + if (this[dependencyList][part.name].indexOf(dep.name) === -1) + this[dependencyList][part.name].push(dep.name) +} + +/** + * Resolves the draft order based on the configuation + * + * @private + * @param {object} graph - The object of resolved dependencies, used to call itself recursively + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__resolveDraftOrder = function () { + this.__draftOrder = Object.keys(this.parts).sort( + (p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1] + ) + + return this.__draftOrder +} + +/** + * Sets visibility of a dependency based on its config + * + * @private + * @param {Part} part - The part of which this is a dependency + * @param {string} name - The name of the part + * @param {string} depName - The name of the dependency + * @param {int} set - The index of the set in the list of settings + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__setFromHide = function (part, name, depName) { + if ( + part.hideDependencies || + part.hideAll || + this.__mutated.partHide[name] || + this.__mutated.partHideAll[name] + ) { + this.__mutated.partHide[depName] = true + this.__mutated.partHideAll[depName] = true + } + + return this +} + +/** + * Sets visibility of an 'after' dependency based on its config + * + * @private + * @param {Part} part - The part of which this is a dependency + * @param {string} name - The name of the part + * @param {string} depName - The name of the dependency + * @param {int} set - The index of the set in the list of settings + * @return {Pattern} this - The Pattern instance + */ +PatternConfig.prototype.__setAfterHide = function (part, name, depName) { + if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) { + this.__mutated.partHide[depName] = true + this.__mutated.partHideAll[depName] = true + } + + return this +} diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index 8f5c76621d7..036d91e89bd 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -32,12 +32,12 @@ describe('Pattern', () => { 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.__inject).to.equal('object') - expect(typeof pattern.__dependencies).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(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('') @@ -145,7 +145,7 @@ describe('Pattern', () => { }) it('Pattern.__init() should resolve parts', () => { - expect(pattern.designConfig.parts.length).to.equal(3) + expect(Object.keys(pattern.config.parts)).to.have.lengthOf(3) }) it('Pattern.__init() should resolve plugins', () => { @@ -324,14 +324,14 @@ describe('Pattern', () => { expect(pattern.config.options.optionR.list[1]).to.equal('green') expect(pattern.config.options.optionR.list[2]).to.equal('blue') // Dependencies - expect(pattern.__dependencies.partB).to.include('partA') - expect(pattern.__dependencies.partC).to.include('partB') - expect(pattern.__dependencies.partR).to.include('partC') - expect(pattern.__dependencies.partR).to.include('partA') + expect(pattern.config.directDependencies.partB).to.include('partA') + expect(pattern.config.directDependencies.partC).to.include('partB') + expect(pattern.config.directDependencies.partR).to.include('partC') + expect(pattern.config.directDependencies.partR).to.include('partA') // Inject - expect(pattern.__inject.partB).to.equal('partA') - expect(pattern.__inject.partC).to.equal('partB') - expect(pattern.__inject.partR).to.equal('partA') + expect(pattern.config.inject.partB).to.equal('partA') + expect(pattern.config.inject.partC).to.equal('partB') + expect(pattern.config.inject.partR).to.equal('partA') // Draft order expect(pattern.config.draftOrder[0]).to.equal('partA') expect(pattern.config.draftOrder[1]).to.equal('partB') @@ -472,12 +472,12 @@ describe('Pattern', () => { expect(pattern.config.options.optionD.list[1]).to.equal('green') expect(pattern.config.options.optionD.list[2]).to.equal('blue') // Dependencies - expect(pattern.__dependencies.partB[0]).to.equal('partA') - expect(pattern.__dependencies.partC[0]).to.equal('partB') - expect(pattern.__dependencies.partD[0]).to.equal('partC') + expect(pattern.config.directDependencies.partB[0]).to.equal('partA') + expect(pattern.config.directDependencies.partC[0]).to.equal('partB') + expect(pattern.config.directDependencies.partD[0]).to.equal('partC') // Inject - expect(pattern.__inject.partB).to.equal('partA') - expect(pattern.__inject.partC).to.equal('partB') + expect(pattern.config.inject.partB).to.equal('partA') + expect(pattern.config.inject.partC).to.equal('partB') // Draft order expect(pattern.config.draftOrder[0]).to.equal('partA') expect(pattern.config.draftOrder[1]).to.equal('partB') diff --git a/packages/core/tests/pattern-runtime-parts.test.mjs b/packages/core/tests/pattern-runtime-parts.test.mjs index a3a640e6721..dbe71799dbb 100644 --- a/packages/core/tests/pattern-runtime-parts.test.mjs +++ b/packages/core/tests/pattern-runtime-parts.test.mjs @@ -12,31 +12,31 @@ describe('Pattern', () => { const part2 = { name: 'test2', - from: part1, + after: part1, draft: ({ part }) => part, } const part3 = { name: 'test3', - after: part2, + from: part2, draft: ({ part }) => part, } describe('with resolveImmediately: true', () => { - it('Should add the part to the internal part object', () => { + it('Should add the part to parts object', () => { const design = new Design({ parts: [part1] }) const pattern = new design() pattern.__init() pattern.addPart(part2, true) - expect(pattern.__designParts.test2).to.equal(part2) + expect(pattern.config.parts.test2).to.equal(part2) }) it('Should resolve injected dependencies for the new part', () => { const design = new Design({ parts: [part1] }) const pattern = new design() pattern.__init() - pattern.addPart(part2, true) - expect(pattern.__inject.test2).to.equal('test') + pattern.addPart(part3, true) + expect(pattern.config.inject.test3).to.equal('test2') }) it('Should resolve all dependencies for the new part', () => { @@ -45,7 +45,7 @@ describe('Pattern', () => { pattern.__init() pattern.addPart(part3, true) expect(pattern.config.resolvedDependencies.test3).to.have.members(['test', 'test2']) - expect(pattern.__designParts.test2).to.equal(part2) + expect(pattern.config.parts.test2).to.equal(part2) }) it('Should add a the measurements for the new part', () => { @@ -79,7 +79,7 @@ describe('Pattern', () => { expect(pattern.config.plugins.testPlugin).to.equal(plugin) }) - it('Should add the options for the new part', () => { + it('Should resolve the options for the new part', () => { const design = new Design({ parts: [part1] }) const pattern = new design() pattern.__init() @@ -96,6 +96,61 @@ describe('Pattern', () => { pattern.addPart(part2, true) expect(pattern.config.options.opt1).to.equal(opt1) }) + + it('Should resolve the dependency options for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const opt1 = { pct: 10, min: 0, max: 50 } + const part2 = { + name: 'test2', + options: { + opt1, + }, + draft: ({ part }) => part, + } + + const part3 = { + name: 'test3', + from: part2, + draft: ({ part }) => part, + } + + pattern.addPart(part3, true) + expect(pattern.config.options.opt1).to.equal(opt1) + }) + + it('Should resolve the overwritten options for the new part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.__init() + + const opt1 = { pct: 10, min: 0, max: 50 } + const part2 = { + name: 'test2', + options: { + opt1: { pct: 15, min: 10, max: 55 }, + }, + draft: ({ part }) => part, + } + + const part3 = { + name: 'test3', + from: part2, + options: { + opt1, + }, + draft: ({ part }) => part, + } + + pattern.addPart(part3, true) + expect(pattern.config.options.opt1).to.equal(opt1) + }) + }) + + describe('with resolveImmediately: false', () => { + it('does not create duplications in the configuration') }) }) }) diff --git a/packages/core/tests/snap.test.mjs b/packages/core/tests/snap.test.mjs index fa64a1f9f6b..4c51de52472 100644 --- a/packages/core/tests/snap.test.mjs +++ b/packages/core/tests/snap.test.mjs @@ -42,6 +42,7 @@ describe('Snapped options', () => { snap: [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144], }, }, + draft: ({ part }) => part, } const design = new Design({ parts: [part] }) const patternA = new design({ options: { test: 0.13 }, measurements }).draft() @@ -67,6 +68,7 @@ describe('Snapped options', () => { }, }, }, + draft: ({ part }) => part, } const design = new Design({ parts: [part] }) const patternA = new design({ options: { test: 0.13 }, measurements, units: 'metric' }).draft() @@ -94,6 +96,7 @@ describe('Snapped options', () => { }, }, }, + draft: ({ part }) => part, } const design = new Design({ parts: [part] }) const patternA = new design({ From 0cd2ad2b8595115e556e4d514af5d8313be4f8c7 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Wed, 22 Feb 2023 16:09:43 +0200 Subject: [PATCH 13/67] document and refactor --- packages/core/src/pattern.mjs | 71 ++++-- packages/core/src/patternConfig.mjs | 353 +++++++++++++++++----------- packages/core/src/store.mjs | 1 + 3 files changed, 260 insertions(+), 165 deletions(-) diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index d593c360304..6c4ce537588 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -26,6 +26,12 @@ import cloneDeep from 'lodash.clonedeep' * @return {object} this - The Pattern instance */ export function Pattern(designConfig = {}) { + // Enumerable properties + this.designConfig = cloneDeep(designConfig) // The design configuration (unresolved) + this.config = {} // Will hold the resolved pattern after calling __init() + this.store = new Store() // Pattern-wide store + this.setStores = [] // Per-set stores + // Non-enumerable properties __addNonEnumProp(this, 'plugins', {}) __addNonEnumProp(this, 'width', 0) @@ -42,13 +48,6 @@ export function Pattern(designConfig = {}) { __addNonEnumProp(this, 'config.parts', {}) __addNonEnumProp(this, 'config.resolvedDependencies', {}) __addNonEnumProp(this, '__storeMethods', new Set()) - __addNonEnumProp(this, '__hide', {}) - - // Enumerable properties - this.designConfig = cloneDeep(designConfig) // The design configuration (unresolved) - this.config = {} // Will hold the resolved pattern after calling __init() - this.store = new Store() // Pattern-wide store - this.setStores = [] // Per-set stores __addNonEnumProp(this, '__configResolver', new PatternConfig(this)) // handles config resolution during __init() as well as runtime part adding return this @@ -59,9 +58,12 @@ export function Pattern(designConfig = {}) { ////////////////////////////////////////////// /** - * FIXME: Allows adding parts to the config at runtime + * Allows adding parts to the config at runtime * * @param {object} part - The part to add + * @param {boolean} resolveImmediately - Should the part be resolved now, or wait until the next call to {@link __init()}? + * It is useful to resolve immediately if one part is being added at runtime + * It might be useful to not resolve immediately if a number of parts will be added over multiple calls * @return {object} this - The Pattern instance */ Pattern.prototype.addPart = function (part, resolveImmediately = false) { @@ -443,24 +445,6 @@ Pattern.prototype.__createStackWithContext = function (name) { return stack } -/** - * Filter optional measurements out id they are also required measurments - * - * @private - * @return {Pattern} this - The Pattern instance - */ -Pattern.prototype.__filterOptionalMeasurements = function () { - if (!this.config.optionalMeasurements) { - this.config.optionalMeasurements = [] - return this - } - this.config.optionalMeasurements = this.config.optionalMeasurements.filter( - (m) => this.config.measurements.indexOf(m) === -1 - ) - - return this -} - /** * Initializes the pattern coniguration and settings * @@ -484,6 +468,7 @@ 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 @@ -602,6 +587,40 @@ Pattern.prototype.__loadConfigData = function () { return this } +/** + * Merges defaults for options with user-provided options + * + * @private + * @return {Pattern} this - The Pattern instance + */ +Pattern.prototype.__loadOptionDefaults = function () { + if (!this.config.options) this.config.options = {} + if (Object.keys(this.config.options).length < 1) return this + for (const i in this.settings) { + for (const [name, option] of Object.entries(this.config.options)) { + // Don't overwrite user-provided settings.options + if (typeof this.settings[i].options[name] === 'undefined') { + if (typeof option === 'object') { + if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100 + else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm + else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg + else if (typeof option.count !== 'undefined') + this.settings[i].options[name] = option.count + else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool + else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt + else { + let err = 'Unknown option type: ' + JSON.stringify(option) + this.store.log.error(err) + throw new Error(err) + } + } else this.settings[i].options[name] = option + } + } + } + + return this +} + /** * Loads a plugin * diff --git a/packages/core/src/patternConfig.mjs b/packages/core/src/patternConfig.mjs index a28abb1b432..79fa1083cb4 100644 --- a/packages/core/src/patternConfig.mjs +++ b/packages/core/src/patternConfig.mjs @@ -1,41 +1,72 @@ import { __addNonEnumProp } from './utils.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) { - if (Array.isArray(plugin)) { - if (plugin[0].name) return plugin[0].name - if (plugin[0].plugin.name) return plugin[0].plugin.name - } else { - if (plugin.name) return plugin.name - if (plugin.plugin?.name) return plugin.plugin.name - } + const toCheck = Array.isArray(plugin) ? plugin[0] : plugin + return toCheck.name || toCheck.plugin?.name || false return false } +///////////////// +// CONSTRUCTOR // +///////////////// +/** + * A class for handling config resolution for a Pattern + * @class + * @param {Pattern} pattern the pattern whose config is being handled + */ export function PatternConfig(pattern) { - this.pattern = pattern + /** @type {Store} the pattern's store, for logging */ this.store = pattern.store - __addNonEnumProp(this, 'plugins', { ...(pattern.designConfig.plugins || {}) }) - __addNonEnumProp(this, 'options', { ...(pattern.designConfig.options || {}) }) - __addNonEnumProp(this, 'measurements', [...(pattern.designConfig.measurements || [])]) - __addNonEnumProp(this, 'optionalMeasurements', [ - ...(pattern.designConfig.optionalMeasurements || []), - ]) - __addNonEnumProp(this, 'inject', {}) - __addNonEnumProp(this, 'directDependencies', {}) - __addNonEnumProp(this, 'resolvedDependencies', {}) - __addNonEnumProp(this, 'parts', {}) + + /** @type {Object} resolved plugins keyed by name */ + this.plugins = { ...(pattern.designConfig.plugins || {}) } + /** @type {Object} resolved options keyed by name */ + this.options = { ...(pattern.designConfig.options || {}) } + /** @type {string[]} required measurements */ + this.measurements = [...(pattern.designConfig.measurements || [])] + /** @type {string[]} optional measurements */ + this.optionalMeasurements = [...(pattern.designConfig.optionalMeasurements || [])] + /** @type {Object} the names of the parts that will be injected */ + this.inject = {} + /** @type {Object} arrays of parts that are direct dependencies of the key */ + this.directDependencies = {} + /** @type {Object} arrays of all dependencies of the key */ + this.resolvedDependencies = {} + /** @type {Object} parts to include in the pattern */ + this.parts = {} + /** @type {Object} which parts are hidden */ + this.partHide = {} + /** @type {Object} which parts hide all their dependencies */ + this.partHideAll = {} + + /** to track which parts have already been resolved */ __addNonEnumProp(this, '__resolvedParts', {}) + /** @type {Object} to track when to overwrite options */ __addNonEnumProp(this, '__mutated', { optionDistance: {}, partDistance: {}, - partHide: {}, - partHideAll: {}, }) } +/** @type {Boolean} change me to true to get full debugging of the resolution process */ const DISTANCE_DEBUG = false +//////////////////// +// PUBLIC METHODs // +//////////////////// + +/** + * Validate that a part meets the requirements to be added to the pattern + * @param {Object} part a part configuration + * @return {boolean} whether the part is valid + */ PatternConfig.prototype.validatePart = function (part) { if (typeof part?.draft !== 'function') { this.store.log.error(`Part must have a draft() method`) @@ -49,12 +80,18 @@ PatternConfig.prototype.validatePart = function (part) { return true } + +/** + * Chainable method to add a part to the configuration + * @param {Object} part + */ PatternConfig.prototype.addPart = function (part) { - if (this.validatePart(part)) this.__resolvePart([part]) + if (this.validatePart(part)) this.__addPart([part]) return this } +/** Log the final report on part inheritance order */ PatternConfig.prototype.logPartDistances = function () { for (const partName in this.parts) { let qualifier = DISTANCE_DEBUG ? 'final' : '' @@ -64,6 +101,10 @@ PatternConfig.prototype.logPartDistances = function () { } } +/** + * Return a configuration in the structure expected by the pattern + * @return {Object} contains parts, plugins, measurements, options, optionalMeasurements, resolvedDependencies, directDependencies, inject, draftOrder, partHide, and partHideAll + */ PatternConfig.prototype.asConfig = function () { return { parts: this.parts, @@ -75,17 +116,30 @@ PatternConfig.prototype.asConfig = function () { directDependencies: this.directDependencies, inject: this.inject, draftOrder: this.__resolveDraftOrder(), - partHide: this.__mutated.partHide, - partHideAll: this.__mutated.partHideAll, + partHide: this.partHide, + partHideAll: this.partHideAll, } } -PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) { +///////////////////// +// PRIVATE METHODS // +///////////////////// + +/** + * Add a part's configuration + * Uses recursion to also add that part's dependencies + * @private + * @param {Object[]} depChain an array starting with the current part to add and containing its dependents/descendents in order + */ +PatternConfig.prototype.__addPart = function (depChain) { + // the current part is the head of the chain const part = depChain[0] - if (distance === 0) { + // the longer the chain, the deeper the part is down it + const distance = depChain.length + if (!this.parts[part.name]) { this.parts[part.name] = Object.freeze(part) } - distance++ + // if it hasn't been registered with a distance, do that now if (typeof this.__mutated.partDistance[part.name] === 'undefined') { this.__mutated.partDistance[part.name] = distance @@ -97,10 +151,11 @@ PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) { // Hide when hideAll is set if (part.hideAll) { - this.__mutated.partHide[part.name] = true + this.partHide[part.name] = true } - this.__resolvePartDependencies(depChain, distance) + // resolve its dependencies + this.__resolvePartDependencies(depChain) // add the part's config this.__addPartConfig(part) @@ -111,20 +166,16 @@ PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) { * * @private * @param {Part} part - The part of which to resolve the config - * @param {onject} config - The global config - * @param {Store} store - The store, used for logging - * @return {object} config - The mutated global config + * @return this */ PatternConfig.prototype.__addPartConfig = function (part) { + // don't resolve a part that's already been resolved if (this.__resolvedParts[part.name]) return this - // Add parts, using set to keep them unique in the array - // this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)] - - return this.__addPartOptions(part) - .__addPartMeasurements(part, true) - .__addPartMeasurements(part, false) - .__addPartPlugins(part) + return this.__addPartOptions(part) // add options + .__addPartMeasurements(part, false) // add required measurements + .__addPartMeasurements(part, true) // add optional measurements + .__addPartPlugins(part) // add plugins } /** @@ -132,29 +183,38 @@ PatternConfig.prototype.__addPartConfig = function (part) { * * @private * @param {Part} part - The part of which to resolve the config - * @return {Pattern} this - The Pattern instance + * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartOptions = function (part) { + // skip empty options if (!part.options) return this + // get the part's option priority const partDistance = this.__mutated.partDistance?.[part.name] || 0 + + // loop through options for (const optionName in part.options) { const option = part.options[optionName] + // get the priority of this option's current registration const optionDistance = this.__mutated.optionDistance[optionName] + // debug the comparison if (optionDistance && DISTANCE_DEBUG) this.store.log.debug( `optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__` ) + + // if it's never been registered, or it's registered at a further distance if (!optionDistance || optionDistance > partDistance) { - this.__mutated.optionDistance[optionName] = partDistance - // Keep design parts immutable in the pattern or risk subtle bugs + // Keep options immutable in the pattern or risk subtle bugs this.options[optionName] = Object.freeze(option) + // register the new distance + this.__mutated.optionDistance[optionName] = partDistance + // debug appropriately this.store.log.debug( optionDistance ? `🟣 __${optionName}__ option overwritten by \`${part.name}\`` : `🔵 __${optionName}__ option loaded from part \`${part.name}\`` ) - this.__loadOptionDefault(optionName, option) } } @@ -166,22 +226,32 @@ PatternConfig.prototype.__addPartOptions = function (part) { * * @private * @param {Part} part - The part of which to resolve the config - * @param {array} list - The list of resolved measurements - * @return {Pattern} this - The Pattern instance + * @param {boolean} optional - are these measurements optional? + * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartMeasurements = function (part, optional = false) { + // which list are we drawing from? const listType = optional ? 'optionalMeasurements' : 'measurements' + // if the part has measurements of this type, go through them if (part[listType]) { part[listType].forEach((m) => { + // we need to know what lists it's already present on const isInReqList = this.measurements.indexOf(m) !== -1 + // if it's already registered as required, we're done here + if (isInReqList) return + + // check if it's registered as optional const optInd = this.optionalMeasurements.indexOf(m) const isInOptList = optInd !== -1 - if (isInReqList) return + // if it is optional and not in the list, push it if (optional && !isInOptList) this.optionalMeasurements.push(m) + // if it's not optional if (!optional) { + // push it to required list this.measurements.push(m) + // make sure it's not also registered as optional if (isInOptList) this.optionalMeasurements.splice(optInd, 1) } @@ -199,7 +269,7 @@ PatternConfig.prototype.__addPartMeasurements = function (part, optional = false * * @private * @param {Part} part - The part of which to resolve the config - * @return {Pattern} this - The Pattern instance + * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartPlugins = function (part) { if (!part.plugins) return this @@ -255,109 +325,117 @@ PatternConfig.prototype.__addPartPlugins = function (part) { return this } -PatternConfig.prototype.__loadOptionDefault = function (optionName, option) { - this.pattern.settings.forEach((set) => { - if (typeof set.options[optionName] !== 'undefined') return - if (typeof option === 'object') { - if (typeof option.pct !== 'undefined') set.options[optionName] = option.pct / 100 - else if (typeof option.mm !== 'undefined') set.options[optionName] = option.mm - else if (typeof option.deg !== 'undefined') set.options[optionName] = option.deg - else if (typeof option.count !== 'undefined') set.options[optionName] = option.count - else if (typeof option.bool !== 'undefined') set.options[optionName] = option.bool - else if (typeof option.dflt !== 'undefined') set.options[optionName] = option.dflt - else { - let err = 'Unknown option type: ' + JSON.stringify(option) - this.store.log.error(err) - throw new Error(err) - } - } else set.options[optionName] = option - }) -} - -PatternConfig.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) { - const part = depChain[0] - - this.parts[dependency.name] = Object.freeze(dependency) - this.__addDependency('directDependencies', part, dependency) - - depChain.forEach((c) => this.__addDependency('resolvedDependencies', c, dependency)) - - switch (depType) { - case 'from': - this.__setFromHide(part, part.name, dependency.name) - this.inject[part.name] = dependency.name - break - case 'after': - this.__setAfterHide(part, part.name, dependency.name) - } -} - -PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) { - const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1 - let didChange = false - if (!this.directDependencies[partName]) return false - this.directDependencies[partName].forEach((dependency) => { - if ( - typeof this.__mutated.partDistance[dependency] === 'undefined' || - this.__mutated.partDistance[dependency] < proposed_dependent_part_distance - ) { - didChange = true - this.__mutated.partDistance[dependency] = proposed_dependent_part_distance - this.__resolveMutatedPartDistance(dependency) - } - if (DISTANCE_DEBUG) - this.store.log.debug( - `partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` - ) - }) - - return didChange -} - +// the two types of dependencies const depTypes = ['from', 'after'] -PatternConfig.prototype.__resolvePartDependencies = function (depChain, distance) { - // Resolve part Dependencies. first from then after + +/** + * Recursively register part dependencies + * triggers {@link __addPart} on new parts found during resolution + * @param {Object[]} depChain an array starting with the current part to register and containing its dependents/descendents in order + * @return {PatternConfig} this + * @private + */ +PatternConfig.prototype.__resolvePartDependencies = function (depChain) { + // the current part is the head of the chain const part = depChain[0] + // get or make its array of resolved dependencies this.resolvedDependencies[part.name] = this.resolvedDependencies[part.name] || [] + // for each dependency type (from, after) depTypes.forEach((d) => { + // if the part has dependencies of that type if (part[d]) { if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`) + // enforce an array const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]] + // each dependency depsOfType.forEach((dot) => { - let count = Object.keys(this.parts).length - // if any changes resulted from resolving this part mutation - this.__resolvePartDependencyChain(depChain, dot, d) - // if a new part was added, resolve the part - const newCount = Object.keys(this.parts).length - if (count < newCount) { - this.__resolvePart([dot, ...depChain], distance) - count = newCount + // add it as a direct dependency of the current part + this.__addDependency('directDependencies', part.name, dot.name) + // add it as a resolved dependency of all parts in the chain + depChain.forEach((c) => this.__addDependency('resolvedDependencies', c.name, dot.name)) + + // handle hiding and injecting + this.__handlePartDependencyOfType(part, dot.name, d) + + // if the dependency isn't registered, register it + if (!this.parts[dot.name]) { + // add the part's configuration + this.__addPart([dot, ...depChain]) } }) } }) + // now that the chain has been registered, recalculate the part distances this.__resolveMutatedPartDistance(part.name) } /** - * Adds a part as a simple dependency - * + * Adds a part as either a direct or a resolved dependency + * @param {string} dependencyList which list to add the part to, 'resolvedDependencies' or 'directDependencies' + * @param {string} partName the name of the part to add the dependency to in the list + * @param {string} depName the name of the dependency to add to the list * @private - * @param {string} name - The name of the dependency - * @param {object} part - The part configuration - * @param {object} dep - The dependency configuration - * @return {object} this - The Pattern instance */ -PatternConfig.prototype.__addDependency = function (dependencyList, part, dep) { - this[dependencyList][part.name] = this[dependencyList][part.name] || [] +PatternConfig.prototype.__addDependency = function (dependencyList, partName, depName) { + this[dependencyList][partName] = this[dependencyList][partName] || [] if (dependencyList == 'resolvedDependencies' && DISTANCE_DEBUG) - this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`) - if (this[dependencyList][part.name].indexOf(dep.name) === -1) - this[dependencyList][part.name].push(dep.name) + this.store.log.debug(`add ${depName} to ${partName} dependencyResolution`) + if (this[dependencyList][partName].indexOf(depName) === -1) + this[dependencyList][partName].push(depName) +} + +/** + * Handle dependency-type specific config business + * @param {Object} part the part to add the dependency to + * @param {string} depName the name of the dependency to add + * @param {string} depType the type of dependency, 'from' or 'after' + * @private + */ +PatternConfig.prototype.__handlePartDependencyOfType = function (part, depName, depType) { + switch (depType) { + case 'from': + this.__setFromHide(part, depName) + this.inject[part.name] = depName + break + case 'after': + this.__setAfterHide(part, depName) + } +} + +/** + * Resolve part option priority + * Recursively bumps priorities down the dependency chain + * @param {string} partName the name of the part to resolve + * @private + */ +PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) { + // if the part has no dependencies, bail + if (!this.directDependencies[partName]) return + + // propose that each of the part's direct dependencies should be at a distance 1 further than the part's distance + const proposed_dependency_distance = this.__mutated.partDistance[partName] + 1 + // check each direct dependency + this.directDependencies[partName].forEach((dependency) => { + // if the dependency doesn't have a distance, or that distance is less than the proposal + if ( + typeof this.__mutated.partDistance[dependency] === 'undefined' || + this.__mutated.partDistance[dependency] < proposed_dependency_distance + ) { + // set the new distance + this.__mutated.partDistance[dependency] = proposed_dependency_distance + // bump the dependency's dependencies as well + this.__resolveMutatedPartDistance(dependency) + } + + if (DISTANCE_DEBUG) + this.store.log.debug( + `partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__` + ) + }) } /** @@ -376,24 +454,22 @@ PatternConfig.prototype.__resolveDraftOrder = function () { } /** - * Sets visibility of a dependency based on its config + * Sets visibility of a 'from' dependency based on its config * * @private * @param {Part} part - The part of which this is a dependency - * @param {string} name - The name of the part * @param {string} depName - The name of the dependency - * @param {int} set - The index of the set in the list of settings * @return {Pattern} this - The Pattern instance */ -PatternConfig.prototype.__setFromHide = function (part, name, depName) { +PatternConfig.prototype.__setFromHide = function (part, depName) { if ( part.hideDependencies || part.hideAll || - this.__mutated.partHide[name] || - this.__mutated.partHideAll[name] + this.partHide[part.name] || + this.partHideAll[part.name] ) { - this.__mutated.partHide[depName] = true - this.__mutated.partHideAll[depName] = true + this.partHide[depName] = true + this.partHideAll[depName] = true } return this @@ -404,15 +480,14 @@ PatternConfig.prototype.__setFromHide = function (part, name, depName) { * * @private * @param {Part} part - The part of which this is a dependency - * @param {string} name - The name of the part * @param {string} depName - The name of the dependency * @param {int} set - The index of the set in the list of settings * @return {Pattern} this - The Pattern instance */ -PatternConfig.prototype.__setAfterHide = function (part, name, depName) { - if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) { - this.__mutated.partHide[depName] = true - this.__mutated.partHideAll[depName] = true +PatternConfig.prototype.__setAfterHide = function (part, depName) { + if (this.partHide[part.name] || this.partHideAll[part.name]) { + this.partHide[depName] = true + this.partHideAll[depName] = true } return this diff --git a/packages/core/src/store.mjs b/packages/core/src/store.mjs index db0bfbffeee..f88f0e5f343 100644 --- a/packages/core/src/store.mjs +++ b/packages/core/src/store.mjs @@ -38,6 +38,7 @@ export function Store(methods = []) { logs.warning.push(...data) }, error: function (...data) { + if (typeof window !== 'undefined') console.error(...data) logs.error.push(...data) }, } From 9be811bd98508f8141871cd5987d7d7ae3605f83 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Thu, 23 Feb 2023 08:13:44 +0200 Subject: [PATCH 14/67] lint fixes and rename --- .../core/src/{patternConfig.mjs => pattern-config.mjs} | 9 ++++++--- packages/core/src/pattern.mjs | 8 ++------ packages/core/tests/pattern-init.test.mjs | 4 ++++ packages/core/tests/pattern-other.test.mjs | 9 ++++++--- 4 files changed, 18 insertions(+), 12 deletions(-) rename packages/core/src/{patternConfig.mjs => pattern-config.mjs} (97%) diff --git a/packages/core/src/patternConfig.mjs b/packages/core/src/pattern-config.mjs similarity index 97% rename from packages/core/src/patternConfig.mjs rename to packages/core/src/pattern-config.mjs index 79fa1083cb4..4dc0b91be08 100644 --- a/packages/core/src/patternConfig.mjs +++ b/packages/core/src/pattern-config.mjs @@ -9,8 +9,6 @@ import { __addNonEnumProp } from './utils.mjs' export function getPluginName(plugin) { const toCheck = Array.isArray(plugin) ? plugin[0] : plugin return toCheck.name || toCheck.plugin?.name || false - - return false } ///////////////// @@ -362,8 +360,13 @@ PatternConfig.prototype.__resolvePartDependencies = function (depChain) { // if the dependency isn't registered, register it if (!this.parts[dot.name]) { - // add the part's configuration + // add the part's configuration. this will recursively add the part's dependencies to all parts in the chain this.__addPart([dot, ...depChain]) + } else { + // if it's already registered, recursion won't happen, but we still need to add its resolved dependencies to all parts in the chain + this.resolvedDependencies[dot.name].forEach((r) => { + depChain.forEach((c) => this.__addDependency('resolvedDependencies', c.name, r)) + }) } }) } diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index 6c4ce537588..a3ba506cf0e 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -11,7 +11,7 @@ import { Store } from './store.mjs' import { Hooks } from './hooks.mjs' import { version } from '../data.mjs' import { __loadPatternDefaults } from './config.mjs' -import { PatternConfig, getPluginName } from './patternConfig.mjs' +import { PatternConfig, getPluginName } from './pattern-config.mjs' import cloneDeep from 'lodash.clonedeep' ////////////////////////////////////////////// @@ -814,11 +814,7 @@ Pattern.prototype.__needs = function (partName, set = 0) { // Walk the only parts, checking each one for a match in its dependencies for (const part of only) { if (part === partName) return true - if (this.config.resolvedDependencies[part]) { - for (const dependency of this.config.resolvedDependencies[part]) { - if (dependency === partName) return true - } - } + if (this.config.resolvedDependencies[part]?.indexOf(partName) !== -1) return true } return false diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index 036d91e89bd..384ec5aa5e5 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -238,6 +238,10 @@ describe('Pattern', () => { } }) + it( + 'Pattern.__init() should resolve nested dependencies for multiple parts that depend on the same part' + ) + // I am aware this does too much for one unit test, but this is to simplify TDD // we can split it up later it('Pattern.__init() should resolve nested injections', () => { diff --git a/packages/core/tests/pattern-other.test.mjs b/packages/core/tests/pattern-other.test.mjs index 3bdbb23448d..ba69d58a17d 100644 --- a/packages/core/tests/pattern-other.test.mjs +++ b/packages/core/tests/pattern-other.test.mjs @@ -54,11 +54,14 @@ describe('Pattern', () => { name: 'test', draft: ({ part }) => part, } - const design = new Design({ parts: [test] }) + const you = { + name: 'you', + draft: ({ part }) => part, + } + const design = new Design({ parts: [test, you] }) const pattern = new design({ only: ['you'] }) pattern.draft() - expect(pattern.setStores[0].logs.debug.length).to.equal(4) - expect(pattern.setStores[0].logs.debug[3]).to.equal( + expect(pattern.setStores[0].logs.debug).to.include( 'Part `test` is not needed. Skipping draft and setting hidden to `true`' ) }) From 86f98206d964c5ff83ce82cba6d608d81ebf60c9 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Thu, 23 Feb 2023 18:22:22 +0200 Subject: [PATCH 15/67] start of draft queue class --- packages/core/src/pattern-config.mjs | 10 ++--- packages/core/src/pattern-draft-queue.mjs | 42 +++++++++++++++++++ packages/core/src/pattern.mjs | 15 ++++--- .../core/tests/pattern-runtime-parts.test.mjs | 5 +++ 4 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/pattern-draft-queue.mjs diff --git a/packages/core/src/pattern-config.mjs b/packages/core/src/pattern-config.mjs index 4dc0b91be08..2f6e0f6a94b 100644 --- a/packages/core/src/pattern-config.mjs +++ b/packages/core/src/pattern-config.mjs @@ -65,7 +65,7 @@ const DISTANCE_DEBUG = false * @param {Object} part a part configuration * @return {boolean} whether the part is valid */ -PatternConfig.prototype.validatePart = function (part) { +PatternConfig.prototype.isPartValid = function (part) { if (typeof part?.draft !== 'function') { this.store.log.error(`Part must have a draft() method`) return false @@ -84,7 +84,7 @@ PatternConfig.prototype.validatePart = function (part) { * @param {Object} part */ PatternConfig.prototype.addPart = function (part) { - if (this.validatePart(part)) this.__addPart([part]) + if (this.isPartValid(part)) this.__addPart([part]) return this } @@ -134,9 +134,9 @@ PatternConfig.prototype.__addPart = function (depChain) { const part = depChain[0] // the longer the chain, the deeper the part is down it const distance = depChain.length - if (!this.parts[part.name]) { - this.parts[part.name] = Object.freeze(part) - } + if (!this.parts[part.name]) this.parts[part.name] = Object.freeze(part) + else return + // if it hasn't been registered with a distance, do that now if (typeof this.__mutated.partDistance[part.name] === 'undefined') { this.__mutated.partDistance[part.name] = distance diff --git a/packages/core/src/pattern-draft-queue.mjs b/packages/core/src/pattern-draft-queue.mjs new file mode 100644 index 00000000000..db8355459a4 --- /dev/null +++ b/packages/core/src/pattern-draft-queue.mjs @@ -0,0 +1,42 @@ +export function PatternDraftQueue(pattern) { + this.__configResolver = pattern.__configResolver + this.queue = this.__resolveDraftOrder() + this.start() +} + +PatternDraftQueue.prototype.start = function () { + this.queueIndex = 0 +} + +PatternDraftQueue.prototype.addPart = function (partName) { + this.queue.push(partName) + return this +} + +PatternDraftQueue.prototype.hasNext = function () { + return this.queueIndex < this.queue.length +} + +PatternDraftQueue.prototype.peek = function () { + return this.queue[this.queueIndex] +} + +PatternDraftQueue.prototype.next = function () { + const next = this.peek() + this.queueIndex++ + return next +} + +/** + * Resolves the draft order based on the configuation + * + * @private + * @param {object} graph - The object of resolved dependencies, used to call itself recursively + * @return {Pattern} this - The Pattern instance + */ +PatternDraftQueue.prototype.__resolveDraftOrder = function () { + const partDistances = this.__configResolver.__mutated.partDistance + return Object.keys(this.__configResolver.parts).sort( + (p1, p2) => partDistances[p2] - partDistances[p1] + ) +} diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index a3ba506cf0e..33b260007c7 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -12,6 +12,7 @@ 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' ////////////////////////////////////////////// @@ -67,10 +68,12 @@ export function Pattern(designConfig = {}) { * @return {object} this - The Pattern instance */ Pattern.prototype.addPart = function (part, resolveImmediately = false) { - if (this.__configResolver.validatePart(part) && this.designConfig.parts.indexOf(part) === -1) { + if (this.__configResolver.isPartValid(part) && this.designConfig.parts.indexOf(part) === -1) { this.designConfig.parts.push(part) - if (resolveImmediately) this.__configResolver.addPart(part) - else this.__initialized = false + if (resolveImmediately) { + if (this.__configResolver.addPart(part) && typeof this.draftQueue !== 'undefined') + this.draftQueue.addPart(part.name) + } else this.__initialized = false } return this } @@ -82,6 +85,7 @@ Pattern.prototype.addPart = function (part, resolveImmediately = false) { */ Pattern.prototype.draft = function () { this.__init() + this.draftQueue = new PatternDraftQueue(this) this.__runHooks('preDraft') // Keep container for drafted parts fresh this.parts = [] @@ -100,8 +104,9 @@ Pattern.prototype.draft = function () { // Handle snap for pct options this.__loadAbsoluteOptionsSet(set) - for (const partName of this.config.draftOrder) { - this.createPartForSet(partName, set) + this.draftQueue.start() + while (this.draftQueue.hasNext()) { + this.createPartForSet(this.draftQueue.next(), set) } this.__runHooks('postSetDraft') } diff --git a/packages/core/tests/pattern-runtime-parts.test.mjs b/packages/core/tests/pattern-runtime-parts.test.mjs index dbe71799dbb..4d8587b1142 100644 --- a/packages/core/tests/pattern-runtime-parts.test.mjs +++ b/packages/core/tests/pattern-runtime-parts.test.mjs @@ -22,6 +22,11 @@ describe('Pattern', () => { draft: ({ part }) => part, } + describe('with runtime: true, resolveImmediately: true', () => { + it('adds the part to the current draft cycle') + it('does not add the part to subsequent draft cycles') + }) + describe('with resolveImmediately: true', () => { it('Should add the part to parts object', () => { const design = new Design({ parts: [part1] }) From 46c705bdfa480a7ef69966f1e9351366c8a9534f Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Thu, 23 Feb 2023 19:15:20 +0200 Subject: [PATCH 16/67] add sinon for better testing --- config/dependencies.yaml | 3 + packages/core/package.json | 5 +- packages/core/src/pattern-draft-queue.mjs | 6 +- packages/core/src/pattern.mjs | 7 +- .../core/tests/pattern-runtime-parts.test.mjs | 51 ++++++++++++-- yarn.lock | 67 ++++++++++++++++++- 6 files changed, 128 insertions(+), 11 deletions(-) diff --git a/config/dependencies.yaml b/config/dependencies.yaml index 4dd7c9ba0a9..c437a916497 100644 --- a/config/dependencies.yaml +++ b/config/dependencies.yaml @@ -62,6 +62,9 @@ core: dev: 'eslint': &eslint '8.34.0' 'nyc': '15.1.0' + 'mocha': *mocha + 'chai': *chai + 'sinon': &sinon '^15.0.1' diana: peer: '@freesewing/brian': *freesewing diff --git a/packages/core/package.json b/packages/core/package.json index e6ff0e2940f..bb3a265071c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -60,7 +60,10 @@ }, "devDependencies": { "eslint": "8.34.0", - "nyc": "15.1.0" + "nyc": "15.1.0", + "mocha": "10.0.0", + "chai": "4.2.0", + "sinon": "^15.0.1" }, "files": [ "dist/*", diff --git a/packages/core/src/pattern-draft-queue.mjs b/packages/core/src/pattern-draft-queue.mjs index db8355459a4..30014d57f42 100644 --- a/packages/core/src/pattern-draft-queue.mjs +++ b/packages/core/src/pattern-draft-queue.mjs @@ -9,7 +9,7 @@ PatternDraftQueue.prototype.start = function () { } PatternDraftQueue.prototype.addPart = function (partName) { - this.queue.push(partName) + if (!this.contains(partName)) this.queue.push(partName) return this } @@ -27,6 +27,10 @@ PatternDraftQueue.prototype.next = function () { return next } +PatternDraftQueue.prototype.contains = function (partName) { + return this.queue.indexOf(partName) !== -1 +} + /** * Resolves the draft order based on the configuation * diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index 33b260007c7..5eefc19056f 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -67,8 +67,11 @@ export function Pattern(designConfig = {}) { * It might be useful to not resolve immediately if a number of parts will be added over multiple calls * @return {object} this - The Pattern instance */ -Pattern.prototype.addPart = function (part, resolveImmediately = false) { - if (this.__configResolver.isPartValid(part) && this.designConfig.parts.indexOf(part) === -1) { +Pattern.prototype.addPart = function (part, resolveImmediately = true) { + if ( + this.__configResolver.isPartValid(part) && + !this.designConfig.parts.find((p) => p.name == part.name) + ) { this.designConfig.parts.push(part) if (resolveImmediately) { if (this.__configResolver.addPart(part) && typeof this.draftQueue !== 'undefined') diff --git a/packages/core/tests/pattern-runtime-parts.test.mjs b/packages/core/tests/pattern-runtime-parts.test.mjs index 4d8587b1142..f272afcf495 100644 --- a/packages/core/tests/pattern-runtime-parts.test.mjs +++ b/packages/core/tests/pattern-runtime-parts.test.mjs @@ -1,5 +1,6 @@ import chai from 'chai' import { Design } from '../src/index.mjs' +import sinon from 'sinon' const expect = chai.expect @@ -22,11 +23,6 @@ describe('Pattern', () => { draft: ({ part }) => part, } - describe('with runtime: true, resolveImmediately: true', () => { - it('adds the part to the current draft cycle') - it('does not add the part to subsequent draft cycles') - }) - describe('with resolveImmediately: true', () => { it('Should add the part to parts object', () => { const design = new Design({ parts: [part1] }) @@ -152,6 +148,51 @@ describe('Pattern', () => { pattern.addPart(part3, true) expect(pattern.config.options.opt1).to.equal(opt1) }) + + describe('during drafting', () => { + it('adds the part to the draft queue', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + pattern.use({ + name: 'draftTimePartPlugin', + hooks: { + postPartDraft: (pattern) => { + const newPart = { + name: 'newPartTest', + draft: ({ part }) => part, + } + + pattern.addPart(newPart) + }, + }, + }) + + pattern.draft() + expect(pattern.draftQueue.contains('newPartTest')).to.be.true + }) + it('drafts the part', () => { + const design = new Design({ parts: [part1] }) + const pattern = new design() + const part2Draft = ({ part }) => part + const draftSpy = sinon.spy(part2Draft) + pattern.use({ + name: 'draftTimePartPlugin', + hooks: { + postPartDraft: (pattern) => { + const newPart = { + name: 'newPartTest', + draft: draftSpy, + } + + pattern.addPart(newPart) + }, + }, + }) + + pattern.draft() + expect(draftSpy.calledOnce).to.be.true + }) + }) }) describe('with resolveImmediately: false', () => { diff --git a/yarn.lock b/yarn.lock index cf2e1fe66c5..2915c4f7aae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4751,6 +4751,34 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@10.0.2", "@sinonjs/fake-timers@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" + integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + +"@sinonjs/samsam@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-7.0.1.tgz#5b5fa31c554636f78308439d220986b9523fc51f" + integrity sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" + integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== + "@socket.io/component-emitter@~3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" @@ -12247,6 +12275,11 @@ just-diff@^5.0.1: resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-5.0.3.tgz#4c9c514dec5526b25ab977590e3c39a0cf271554" integrity sha512-a8p80xcpJ6sdurk5PxDKb4mav9MeKjA3zFKZpCWBIfvg8mznfnmb13MKZvlrwJ+Lhis0wM3uGAzE0ArhFHvIcg== +just-extend@^4.0.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" + integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== + jwa@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" @@ -14325,6 +14358,17 @@ next@13.1.6: "@next/swc-win32-ia32-msvc" "13.1.6" "@next/swc-win32-x64-msvc" "13.1.6" +nise@^5.1.2: + version "5.1.4" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0" + integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg== + dependencies: + "@sinonjs/commons" "^2.0.0" + "@sinonjs/fake-timers" "^10.0.2" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + nlcst-to-string@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/nlcst-to-string/-/nlcst-to-string-2.0.4.tgz#9315dfab80882bbfd86ddf1b706f53622dc400cc" @@ -15518,6 +15562,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -18321,6 +18372,18 @@ simple-wcswidth@^1.0.1: resolved "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz#8ab18ac0ae342f9d9b629604e54d2aa1ecb018b2" integrity sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg== +sinon@^15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.0.1.tgz#ce062611a0b131892e2c18f03055b8eb6e8dc234" + integrity sha512-PZXKc08f/wcA/BMRGBze2Wmw50CWPiAH3E21EOi4B49vJ616vW4DQh4fQrqsYox2aNR/N3kCqLuB0PwwOucQrg== + dependencies: + "@sinonjs/commons" "^2.0.0" + "@sinonjs/fake-timers" "10.0.2" + "@sinonjs/samsam" "^7.0.1" + diff "^5.0.0" + nise "^5.1.2" + supports-color "^7.2.0" + sirv@^1.0.7: version "1.0.19" resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" @@ -19106,7 +19169,7 @@ supports-color@^5.3.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -19625,7 +19688,7 @@ type-component@0.0.1: resolved "https://registry.yarnpkg.com/type-component/-/type-component-0.0.1.tgz#952a6c81c21efd24d13d811d0c8498cb860e1956" integrity sha512-mDZRBQS2yZkwRQKfjJvQ8UIYJeBNNWCq+HBNstl9N5s9jZ4dkVYXEGkVPsSCEh5Ld4JM1kmrZTzjnrqSAIQ7dw== -type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== From cd034a7cf3eca49437acea204f2da6f560ba3981 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Thu, 23 Feb 2023 23:30:38 +0200 Subject: [PATCH 17/67] use new parts adding to add cut duplicates at draft time --- .../layout/cut/plugin-cut-layout.mjs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs b/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs index a1a3430b45a..46354c69600 100644 --- a/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs +++ b/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs @@ -1,5 +1,6 @@ const prefix = 'mirroredOnFold' +const redraft = ({ part }) => part export const cutLayoutPlugin = { hooks: { postPartDraft: (pattern) => { @@ -8,13 +9,25 @@ export const cutLayoutPlugin = { const { macro } = pattern.parts[pattern.activeSet][pattern.activePart].shorthand() if (partCutlist.cutOnFold) macro('mirrorOnFold', { fold: partCutlist.cutOnFold }) + + if (partCutlist.materials) { + for (const material in partCutlist.materials) { + for (var i = 1; i < partCutlist.materials[material]; i++) { + pattern.addPart({ + name: `${pattern.activePart}_${material}_${i}`, + from: pattern.activePart, + draft: redraft, + }) + } + } + } }, }, macros: { mirrorOnFold: ({ fold }, { paths, snippets, utils, macro, points }) => { const mirrorPaths = [] for (const p in paths) { - if (!paths[p].hidden) mirrorPaths.push(paths[p]) + if (!paths[p].hidden && !p.startsWith(prefix)) mirrorPaths.push(paths[p]) } const mirrorPoints = [] @@ -34,11 +47,16 @@ export const cutLayoutPlugin = { } } + let unnamed = 0 macro('mirror', { - paths: mirrorPaths, + paths: Object.values(mirrorPaths), points: mirrorPoints, mirror: fold, prefix, + nameFormat: (path) => { + unnamed++ + return `${prefix}_${unnamed}` + }, }) for (var def in snippetsByType) { From 29e3ba63230cc46be4d9429474b82562740d0a98 Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Mon, 27 Feb 2023 17:47:34 -0600 Subject: [PATCH 18/67] cutting layout page has tabs for each fabric type. parts duplication --- config/dependencies.yaml | 1 + designs/bella/src/back.mjs | 3 + designs/carlton/src/back.mjs | 3 + plugins/plugin-bundle/src/index.mjs | 47 ++++--- .../components/workbench/layout/cut/index.mjs | 107 +++++++++++----- .../layout/cut/plugin-cut-layout.mjs | 118 ++++++++++-------- .../workbench/layout/cut/settings.mjs | 82 ++++++------ .../workbench/layout/draft/index.mjs | 7 +- .../workbench/layout/plugin-layout-part.mjs | 3 + .../menu/core-settings/core-setting-only.mjs | 2 +- .../workbench/menu/design-options/option.mjs | 2 +- .../shared/components/wrappers/workbench.mjs | 7 +- 12 files changed, 224 insertions(+), 158 deletions(-) diff --git a/config/dependencies.yaml b/config/dependencies.yaml index c437a916497..fba98bbc1d1 100644 --- a/config/dependencies.yaml +++ b/config/dependencies.yaml @@ -380,6 +380,7 @@ shared: 'lodash.clonedeep': '4.5.0' 'lodash.orderby': *_orderby 'lodash.unset': *_unset + 'lodash.get': *_get 'mdast-util-toc': '6.1.0' 'pdfkit': '0.13.0' 'postcss-for': '2.1.1' diff --git a/designs/bella/src/back.mjs b/designs/bella/src/back.mjs index adb64cd9590..9d4565bf127 100644 --- a/designs/bella/src/back.mjs +++ b/designs/bella/src/back.mjs @@ -65,6 +65,7 @@ export const back = { measurements, log, part, + addCut, }) => { // Get to work points.cbNeck = new Point(0, measurements.neck * options.backNeckCutout) @@ -269,6 +270,8 @@ export const back = { on: ['armholePitch', 'bustCenter'], }) + addCut() + if (sa) paths.sa = paths.saBase.offset(sa).attr('class', 'fabric sa') if (paperless) { diff --git a/designs/carlton/src/back.mjs b/designs/carlton/src/back.mjs index 75ae0808ca0..81181bab8dd 100644 --- a/designs/carlton/src/back.mjs +++ b/designs/carlton/src/back.mjs @@ -16,6 +16,7 @@ function draftCarltonBack({ paths, Path, part, + addCut, }) { calculateRatios(part) // Belt width @@ -95,6 +96,8 @@ function draftCarltonBack({ .line(points.bpStart) .attr('class', 'dashed') + addCut(2) + addCut(2, 'lining') if (complete) { macro('sprinkle', { snippet: 'bnotch', diff --git a/plugins/plugin-bundle/src/index.mjs b/plugins/plugin-bundle/src/index.mjs index d72b9f954fe..d7466568b35 100644 --- a/plugins/plugin-bundle/src/index.mjs +++ b/plugins/plugin-bundle/src/index.mjs @@ -12,6 +12,7 @@ import { roundPlugin } from '../../plugin-round/src/index.mjs' import { scaleboxPlugin } from '../../plugin-scalebox/src/index.mjs' import { sprinklePlugin } from '../../plugin-sprinkle/src/index.mjs' import { titlePlugin } from '../../plugin-title/src/index.mjs' +import { pluginCutlist } from '../../plugin-cutlist/src/index.mjs' import { name, version } from '../data.mjs' const bundledPlugins = [ @@ -29,38 +30,44 @@ const bundledPlugins = [ scaleboxPlugin, sprinklePlugin, titlePlugin, + pluginCutlist, ] -function bundleHooks() { - const hooks = {} - for (const plugin of bundledPlugins) { - for (const i in plugin.hooks) { - if (typeof hooks[i] === 'undefined') hooks[i] = [] - const hook = plugin.hooks[i] - if (typeof hook === 'function') hooks[i].push(hook) - else if (typeof hook === 'object') { - for (let method of hook) hooks[i].push(method) - } +const hooks = {} +const macros = {} +const store = [] + +function bundleHooks(plugin) { + for (const i in plugin.hooks) { + if (typeof hooks[i] === 'undefined') hooks[i] = [] + const hook = plugin.hooks[i] + if (typeof hook === 'function') hooks[i].push(hook) + else if (typeof hook === 'object') { + for (let method of hook) hooks[i].push(method) } } - - return hooks } -function bundleMacros() { - const macros = {} - for (const plugin of bundledPlugins) { - for (const i in plugin.macros) macros[i] = plugin.macros[i] - } +function bundleMacros(plugin) { + for (const i in plugin.macros) macros[i] = plugin.macros[i] +} - return macros +function bundleStore(plugin) { + if (plugin.store) store.push(...plugin.store) +} + +for (const plugin of bundledPlugins) { + bundleHooks(plugin, hooks) + bundleMacros(plugin, macros) + bundleStore(plugin, store) } export const plugin = { name, version, - hooks: bundleHooks(), - macros: bundleMacros(), + hooks, + macros, + store, } // More specifically named exports diff --git a/sites/shared/components/workbench/layout/cut/index.mjs b/sites/shared/components/workbench/layout/cut/index.mjs index eb5355d68ae..6727cf9db05 100644 --- a/sites/shared/components/workbench/layout/cut/index.mjs +++ b/sites/shared/components/workbench/layout/cut/index.mjs @@ -4,8 +4,10 @@ import { Draft } from '../draft/index.mjs' import { fabricPlugin } from '../plugin-layout-part.mjs' import { cutLayoutPlugin } from './plugin-cut-layout.mjs' import { pluginCutlist } from '@freesewing/plugin-cutlist' -import { useEffect } from 'react' import { measurementAsMm } from 'shared/utils.mjs' +import { useState, useEffect, useCallback, useRef } from 'react' +import { Tabs } from 'shared/components/mdx/tabs.mjs' +import get from 'lodash.get' export const CutLayout = (props) => { const { t } = useTranslation(['workbench']) @@ -15,46 +17,89 @@ export const CutLayout = (props) => { if (props.gist?._state?.xray?.enabled) props.updateGist(['_state', 'xray', 'enabled'], false) }) - const draft = props.draft const isImperial = props.gist.units === 'imperial' - const gistSettings = props.gist?._state?.layout?.forCutting?.fabric || {} - // add the pages plugin to the draft - const layoutSettings = { - sheetWidth: gistSettings.sheetWidth || measurementAsMm(isImperial ? 54 : 120, props.gist.units), - sheetHeight: - gistSettings.sheetHeight || measurementAsMm(isImperial ? 36 : 100, props.gist.units), - } - draft.use(fabricPlugin(layoutSettings)) - draft.use(pluginCutlist) - draft.use(cutLayoutPlugin) + const [patternProps, setPatternProps] = useState(undefined) + const [cutFabrics, setCutFabrics] = useState(['fabric']) + const [draft, setDraft] = useState() + const [cutFabric, setCutFabric] = useState('fabric') + + const gistSettings = get(props.gist, ['_state', 'layout', 'forCutting', 'fabric', cutFabric]) + const sheetWidth = + gistSettings?.sheetWidth || measurementAsMm(isImperial ? 54 : 120, props.gist.units) + const gist = props.gist + const sheetHeight = measurementAsMm(isImperial ? 36 : 100, props.gist.units) + + useEffect(() => { + try { + // get the appropriate layout for the view + const layout = gist.layouts?.[gist._state.view]?.[cutFabric] || gist.layout || true + // hand it separately to the design + const draft = new props.design({ ...gist, layout }) + + // add the pages plugin to the draft + const layoutSettings = { + sheetWidth, + sheetHeight, + } + draft.use(fabricPlugin(layoutSettings)) + draft.use(pluginCutlist) + draft.use(cutLayoutPlugin(cutFabric)) + // draft the pattern + draft.draft() + setPatternProps(draft.getRenderProps()) + + const cutList = draft.setStores[0].get('cutlist') + const cf = ['fabric'] + for (const partName in cutList) { + for (const matName in cutList[partName].materials) { + if (!cf.includes(matName)) cf.push(matName) + } + } + setCutFabrics(cf) + } catch (err) { + console.log(err, props.gist) + } + }, [cutFabric, isImperial, gist]) - let patternProps - try { - // draft the pattern - draft.draft() - patternProps = draft.getRenderProps() - } catch (err) { - console.log(err, props.gist) - } const bgProps = { fill: 'url(#page)' } let name = props.design.designConfig.data.name name = name.replace('@freesewing/', '') - return ( + + return patternProps ? (

{t('layoutThing', { thing: name }) + ': ' + t('forCutting')}

- - +
+
+ {cutFabrics.map((title) => ( + + ))} +
+ +
- ) + ) : null } diff --git a/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs b/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs index 46354c69600..34faf9c693b 100644 --- a/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs +++ b/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs @@ -1,70 +1,80 @@ const prefix = 'mirroredOnFold' const redraft = ({ part }) => part -export const cutLayoutPlugin = { - hooks: { - postPartDraft: (pattern) => { - const partCutlist = pattern.setStores[pattern.activeSet].get(['cutlist', pattern.activePart]) - if (!partCutlist) return +export const cutLayoutPlugin = function (material) { + return { + hooks: { + postPartDraft: (pattern) => { + if (pattern.activePart.startsWith('cut.') || pattern.activePart === 'fabric') return - const { macro } = pattern.parts[pattern.activeSet][pattern.activePart].shorthand() - if (partCutlist.cutOnFold) macro('mirrorOnFold', { fold: partCutlist.cutOnFold }) + const partCutlist = pattern.setStores[pattern.activeSet].get([ + 'cutlist', + pattern.activePart, + ]) - if (partCutlist.materials) { - for (const material in partCutlist.materials) { - for (var i = 1; i < partCutlist.materials[material]; i++) { - pattern.addPart({ - name: `${pattern.activePart}_${material}_${i}`, - from: pattern.activePart, - draft: redraft, - }) - } + if (!partCutlist?.materials?.[material] && material !== 'fabric') { + pattern.parts[pattern.activeSet][pattern.activePart].hide() + return } - } + + if (partCutlist?.cutOnFold) { + const { macro } = pattern.parts[pattern.activeSet][pattern.activePart].shorthand() + macro('mirrorOnFold', { fold: partCutlist.cutOnFold }) + } + + for (var i = 1; i < partCutlist?.materials?.[material].cut; i++) { + const dupPartName = `cut.${pattern.activePart}.${material}_${i + 1}` + pattern.addPart({ + name: dupPartName, + from: pattern.config.parts[pattern.activePart], + draft: redraft, + }) + } + }, }, - }, - macros: { - mirrorOnFold: ({ fold }, { paths, snippets, utils, macro, points }) => { - const mirrorPaths = [] - for (const p in paths) { - if (!paths[p].hidden && !p.startsWith(prefix)) mirrorPaths.push(paths[p]) - } + macros: { + mirrorOnFold: ({ fold }, { paths, snippets, utils, macro, points }) => { + const mirrorPaths = [] + for (const p in paths) { + if (!paths[p].hidden && !p.startsWith(prefix)) mirrorPaths.push(paths[p]) + } - const mirrorPoints = [] - const snippetsByType = {} - for (var s in snippets) { - const snip = snippets[s] - if (['logo'].indexOf(snip.def) > -1) continue + const mirrorPoints = [] + const snippetsByType = {} + for (var s in snippets) { + const snip = snippets[s] + if (['logo'].indexOf(snip.def) > -1) continue - snippetsByType[snip.def] = snippetsByType[snip.def] || [] + snippetsByType[snip.def] = snippetsByType[snip.def] || [] - mirrorPoints.push(snip.anchor) - for (var pName in points) { - if (points[pName] === snip.anchor) { - snippetsByType[snip.def].push(prefix + utils.capitalize(pName)) - break + mirrorPoints.push(snip.anchor) + for (var pName in points) { + if (points[pName] === snip.anchor) { + snippetsByType[snip.def].push(prefix + utils.capitalize(pName)) + break + } } } - } - let unnamed = 0 - macro('mirror', { - paths: Object.values(mirrorPaths), - points: mirrorPoints, - mirror: fold, - prefix, - nameFormat: (path) => { - unnamed++ - return `${prefix}_${unnamed}` - }, - }) - - for (var def in snippetsByType) { - macro('sprinkle', { - snippet: def, - on: snippetsByType[def], + let unnamed = 0 + macro('mirror', { + paths: Object.values(mirrorPaths), + points: mirrorPoints, + mirror: fold, + prefix, + nameFormat: (path) => { + unnamed++ + return `${prefix}_${unnamed}` + }, }) - } + + for (var def in snippetsByType) { + macro('sprinkle', { + snippet: def, + on: snippetsByType[def], + }) + } + }, }, - }, + } } diff --git a/sites/shared/components/workbench/layout/cut/settings.mjs b/sites/shared/components/workbench/layout/cut/settings.mjs index 0fe4c2153ed..7156e591e9b 100644 --- a/sites/shared/components/workbench/layout/cut/settings.mjs +++ b/sites/shared/components/workbench/layout/cut/settings.mjs @@ -1,39 +1,32 @@ -import { useMemo, useEffect, useState, useCallback } from 'react' -import { ClearIcon } from 'shared/components/icons.mjs' +import { useMemo, useEffect, useState, useCallback, useRef } from 'react' +import { ClearIcon, PageIcon } from 'shared/components/icons.mjs' import { useTranslation } from 'next-i18next' import { formatFraction128, measurementAsMm, round, formatMm } from 'shared/utils.mjs' +import get from 'lodash.get' -const FabricSizer = ({ gist, updateGist }) => { +const FabricSizer = ({ gist, updateGist, cutFabric, sheetWidth }) => { const { t } = useTranslation(['workbench']) - const [val, setVal] = useState(500) - - useEffect(() => { - setVal(formatMm(gist._state?.layout?.forCutting?.fabric.sheetWidth || 500, gist.units, 'none')) - }, [gist]) - const setFabricWidth = (width) => {} + let val = formatMm(sheetWidth, gist.units, 'none') // onChange - const update = useCallback( - (evt) => { - evt.stopPropagation() - let evtVal = evt.target.value - // set Val immediately so that the input reflects it - setVal(evtVal) + const update = (evt) => { + evt.stopPropagation() + let evtVal = evt.target.value + // set Val immediately so that the input reflects it + val = evtVal - let useVal = measurementAsMm(evtVal, gist.units) - // only set to the gist if it's valid - if (!isNaN(useVal)) { - updateGist(['_state', 'layout', 'forCutting', 'fabric', 'sheetWidth'], useVal) - } - }, - [gist.units] - ) + let useVal = measurementAsMm(evtVal, gist.units) + // only set to the gist if it's valid + if (!isNaN(useVal)) { + updateGist(['_state', 'layout', 'forCutting', 'fabric', cutFabric, 'sheetWidth'], useVal) + } + } return ( -
+
diff --git a/sites/shared/components/workbench/layout/cut/settings.mjs b/sites/shared/components/workbench/layout/cut/settings.mjs index 5d0839f40f0..015d713331a 100644 --- a/sites/shared/components/workbench/layout/cut/settings.mjs +++ b/sites/shared/components/workbench/layout/cut/settings.mjs @@ -1,6 +1,7 @@ import { ClearIcon, IconWrapper } from 'shared/components/icons.mjs' import { useTranslation } from 'next-i18next' import { formatFraction128, measurementAsMm, round, formatMm } from 'shared/utils.mjs' +import { ShowButtonsToggle } from '../draft/buttons.mjs' const SheetIcon = (props) => ( @@ -118,14 +119,21 @@ export const CutLayoutSettings = ({ {fabricLength}
- +
+ + +
) } diff --git a/sites/shared/components/workbench/layout/draft/buttons.mjs b/sites/shared/components/workbench/layout/draft/buttons.mjs index b08762af782..84562e672a3 100644 --- a/sites/shared/components/workbench/layout/draft/buttons.mjs +++ b/sites/shared/components/workbench/layout/draft/buttons.mjs @@ -1,5 +1,6 @@ import { useTranslation } from 'next-i18next' import { ClearIcon } from 'shared/components/icons.mjs' +import get from 'lodash.get' const Triangle = ({ transform = 'translate(0,0)', fill = 'currentColor' }) => ( { ) } +export const ShowButtonsToggle = ({ gist, layoutSetType, updateGist }) => { + const { t } = useTranslation('workbench') + const path = ['_state', 'layout', layoutSetType, 'showButtons'] + const showButtons = get(gist, path, true) + const setShowButtons = () => updateGist(path, !showButtons) + + return ( + + ) +} + /** buttons for manipulating the part */ export const Buttons = ({ transform, flip, rotate, resetPart, rotate90 }) => { const { t } = useTranslation('workbench') diff --git a/sites/shared/components/workbench/layout/draft/index.mjs b/sites/shared/components/workbench/layout/draft/index.mjs index 1a7f27cec7c..abbdef9af38 100644 --- a/sites/shared/components/workbench/layout/draft/index.mjs +++ b/sites/shared/components/workbench/layout/draft/index.mjs @@ -13,6 +13,7 @@ export const Draft = (props) => { bgProps = {}, fitLayoutPart = false, layoutType = 'printingLayout', + layoutSetType = 'forPrinting', } = props const svgRef = useRef(null) @@ -98,6 +99,7 @@ export const Draft = (props) => { gist, updateLayout, isLayoutPart: stackName === props.layoutPart, + layoutSetType: layoutSetType, }} /> ) diff --git a/sites/shared/components/workbench/layout/draft/stack.mjs b/sites/shared/components/workbench/layout/draft/stack.mjs index 048a422e6a6..d5b3469d98b 100644 --- a/sites/shared/components/workbench/layout/draft/stack.mjs +++ b/sites/shared/components/workbench/layout/draft/stack.mjs @@ -50,6 +50,7 @@ import { getProps, angle } from '../../draft/utils.mjs' import { drag } from 'd3-drag' import { select } from 'd3-selection' import { Buttons } from './buttons.mjs' +import get from 'lodash.get' export const Stack = (props) => { const { layout, stack, stackName, gist } = props @@ -230,6 +231,7 @@ export const Stack = (props) => { // don't render if the part is empty // if (Object.keys(part.snippets).length === 0 && Object.keys(part.paths).length === 0) return null; + const showButtons = get(gist, ['_state', 'layout', props.layoutSetType, 'showButtons'], true) return ( @@ -249,17 +251,19 @@ export const Stack = (props) => { id={`${stackName}-layout-rect`} onClick={toggleDragRotate} /> - + {showButtons ? ( + + ) : null} )} diff --git a/sites/shared/components/workbench/layout/print/settings.mjs b/sites/shared/components/workbench/layout/print/settings.mjs index 34fe4888467..ca8374234b5 100644 --- a/sites/shared/components/workbench/layout/print/settings.mjs +++ b/sites/shared/components/workbench/layout/print/settings.mjs @@ -2,6 +2,7 @@ import { PageSizePicker } from './pagesize-picker.mjs' import { PageOrientationPicker } from './orientation-picker.mjs' import { PrintIcon, RightIcon, ClearIcon, ExportIcon } from 'shared/components/icons.mjs' import { useTranslation } from 'next-i18next' +import { ShowButtonsToggle } from '../draft/buttons.mjs' export const PrintLayoutSettings = (props) => { const { t } = useTranslation(['workbench']) @@ -34,6 +35,11 @@ export const PrintLayoutSettings = (props) => {
+