From 0b18d81e14fcdcb6db5fd63d56eb777ca0f29204 Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Sat, 17 Sep 2022 10:24:13 +0200 Subject: [PATCH 1/4] feat(core): Added support for multiple sets of settings This changes they was settings (what the user provides) are handled. Before this, settings were passed as an object and that was it. Now, settings are treated as an array of settings objects and this adds full support for managing multiple sets of settings in a single pattern instance. This is the mechanism that's used for FreeSewing's sampling which used to be a rather hackish implementation, but now merely sets up the relevant list of settings, and then calls `pattern.draft()` as usual. Things to be mindful of is that parts, the store and settings themselves are tied to each set of settings. So where they used to be an object, they are now an array of object. --- packages/core/src/config.mjs | 2 + packages/core/src/design.mjs | 4 +- packages/core/src/index.mjs | 2 + packages/core/src/part.mjs | 32 +- packages/core/src/pattern.mjs | 570 +++++++++++++++++++++------------- packages/core/src/stack.mjs | 11 +- packages/core/src/utils.mjs | 28 +- 7 files changed, 397 insertions(+), 252 deletions(-) diff --git a/packages/core/src/config.mjs b/packages/core/src/config.mjs index d16cee9df4a..9b6115f9624 100644 --- a/packages/core/src/config.mjs +++ b/packages/core/src/config.mjs @@ -2,6 +2,7 @@ export const loadDesignDefaults = () => ({ measurements: [], optionalMeasurements: [], options: {}, + optionDistance: {}, parts: [], data: {}, plugins: [], @@ -10,6 +11,7 @@ export const loadDesignDefaults = () => ({ export const loadPatternDefaults = () => ({ complete: true, idPrefix: 'fs-', + stackPrefix: '', locale: 'en', units: 'metric', margin: 2, diff --git a/packages/core/src/design.mjs b/packages/core/src/design.mjs index 5f8d568c5df..7a06031e18b 100644 --- a/packages/core/src/design.mjs +++ b/packages/core/src/design.mjs @@ -10,12 +10,12 @@ export function Design(config) { config = { ...loadDesignDefaults(), ...config } // Create the pattern constructor - const pattern = function (settings) { + const pattern = function (...sets) { // Pass the design config Pattern.call(this, config) // Pass the pattern settings - return this.__applySettings(settings) + return this.__applySettings(sets) } // Set up inheritance diff --git a/packages/core/src/index.mjs b/packages/core/src/index.mjs index 2707e475770..bc7109f0e08 100644 --- a/packages/core/src/index.mjs +++ b/packages/core/src/index.mjs @@ -1,6 +1,7 @@ import { Attributes } from './attributes.mjs' import { Design } from './design.mjs' import { Pattern } from './pattern.mjs' +import { Part } from './part.mjs' import { Point } from './point.mjs' import { Path } from './path.mjs' import { Snippet } from './snippet.mjs' @@ -44,6 +45,7 @@ export { Pattern, Point, Path, + Part, Snippet, Store, Bezier, diff --git a/packages/core/src/part.mjs b/packages/core/src/part.mjs index 42494aea7f5..d0bc8b17e27 100644 --- a/packages/core/src/part.mjs +++ b/packages/core/src/part.mjs @@ -336,21 +336,21 @@ Part.prototype.shorthand = function () { return shorthand } -Part.prototype.isEmpty = function () { - if (Object.keys(this.snippets).length > 0) return false - - if (Object.keys(this.paths).length > 0) { - for (const p in this.paths) { - if (this.paths[p].render && this.paths[p].length()) return false - } - } - - for (const p in this.points) { - if (this.points[p].attributes.get('data-text')) return false - if (this.points[p].attributes.get('data-circle')) return false - } - - return true -} +//Part.prototype.isEmpty = function () { +// if (Object.keys(this.snippets).length > 0) return false +// +// if (Object.keys(this.paths).length > 0) { +// for (const p in this.paths) { +// if (this.paths[p].render && this.paths[p].length()) return false +// } +// } +// +// for (const p in this.points) { +// if (this.points[p].attributes.get('data-text')) return false +// if (this.points[p].attributes.get('data-circle')) return false +// } +// +// return true +//} export default Part diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index 24c5854db75..cf83c78e48a 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -21,6 +21,7 @@ import { loadPatternDefaults } from './config.mjs' export function Pattern(config) { // Non-enumerable properties addNonEnumProp(this, 'plugins', {}) + addNonEnumProp(this, 'parts', [{}]) addNonEnumProp(this, 'width', 0) addNonEnumProp(this, 'height', 0) addNonEnumProp(this, 'autoLayout', { stacks: {} }) @@ -40,9 +41,8 @@ export function Pattern(config) { // Enumerable properties this.config = config // Design config - this.parts = {} // Drafted parts container this.stacks = {} // Drafted stacks container - this.store = new Store() // Store for sharing data across parts + this.stores = [new Store()] return this } @@ -65,36 +65,42 @@ Pattern.prototype.init = function () { .__loadOptionDefaults() // Merges default options with user provided ones // Say hello - this.store.log.info( - `New \`${this.store.get('data.name', 'No Name')}:` + - `${this.store.get( + this.stores[0].log.info( + `New \`${this.stores[0].get('data.name', 'No Name')}:` + + `${this.stores[0].get( 'data.version', 'No version' )}\` pattern using \`@freesewing/core:${version}\`` ) - this.store.log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`) + this.stores[0].log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`) return this } Pattern.prototype.__loadConfigData = function () { - if (this.config.data) this.store.set('data', this.config.data) + if (this.config.data) { + for (const i in this.settings) this.stores[i].set('data', this.config.data) + } return this } -Pattern.prototype.__createPartWithContext = function (name) { +Pattern.prototype.__createPartWithContext = function (name, set) { // Context object to add to Part closure const part = new Part() part.name = name + part.set = set part.stack = this.__parts[name]?.stack || name part.context = { - parts: this.parts, + parts: this.parts[set], config: this.config, - settings: this.settings, - store: this.store, + settings: this.settings[set], + store: this.stores[set], macros: this.macros, } + if (this.settings[set]?.partClasses) { + part.attr('class', this.settings[set].partClasses) + } for (const macro in this.macros) { part[macroName(macro)] = this.macros[macro] @@ -110,7 +116,7 @@ Pattern.prototype.__createStackWithContext = function (name) { stack.context = { config: this.config, settings: this.settings, - store: this.store, + stores: this.stores, } return stack @@ -119,22 +125,25 @@ Pattern.prototype.__createStackWithContext = function (name) { // Merges default for options with user-provided options Pattern.prototype.__loadOptionDefaults = function () { if (Object.keys(this.config.options).length < 1) return this - for (const [name, option] of Object.entries(this.config.options)) { - // Don't overwrite user-provided settings.options - if (typeof this.settings.options[name] === 'undefined') { - if (typeof option === 'object') { - if (typeof option.pct !== 'undefined') this.settings.options[name] = option.pct / 100 - else if (typeof option.mm !== 'undefined') this.settings.options[name] = option.mm - else if (typeof option.deg !== 'undefined') this.settings.options[name] = option.deg - else if (typeof option.count !== 'undefined') this.settings.options[name] = option.count - else if (typeof option.bool !== 'undefined') this.settings.options[name] = option.bool - else if (typeof option.dflt !== 'undefined') this.settings.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.options[name] = option + for (const i in this.settings) { + for (const [name, option] of Object.entries(this.config.options)) { + // Don't overwrite user-provided settings.options + if (typeof this.settings[i].options[name] === 'undefined') { + if (typeof option === 'object') { + if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100 + else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm + else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg + else if (typeof option.count !== 'undefined') + this.settings[i].options[name] = option.count + else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool + else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt + else { + let err = 'Unknown option type: ' + JSON.stringify(option) + this.stores[i].log.error(err) + throw new Error(err) + } + } else this.settings[i].options[name] = option + } } } @@ -153,12 +162,12 @@ Pattern.prototype.getPartList = function () { return Object.keys(this.config.parts) } -function snappedOption(option, pattern) { - const conf = pattern.config.options[option] - const abs = conf.toAbs(pattern.settings.options[option], pattern.settings) +Pattern.prototype.__snappedPercentageOption = function (optionName, set) { + const conf = this.config.options[optionName] + const abs = conf.toAbs(this.settings[set].options[optionName], this.settings[set]) // Handle units-specific config if (!Array.isArray(conf.snap) && conf.snap.metric && conf.snap.imperial) - conf.snap = conf.snap[pattern.settings.units] + conf.snap = conf.snap[this.settings[set].units] // Simple steps if (typeof conf.snap === 'number') return Math.ceil(abs / conf.snap) * conf.snap // List of snaps @@ -187,7 +196,7 @@ Pattern.prototype.runHooks = function (hookName, data = false) { if (data === false) data = this let hooks = this.hooks[hookName] if (hooks.length > 0) { - this.store.log.debug(`Running \`${hookName}\` hooks`) + this.stores[0].log.debug(`Running \`${hookName}\` hooks`) for (let hook of hooks) { hook.method(data, hook.data) } @@ -202,9 +211,30 @@ Pattern.prototype.addPart = function (part) { if (part.name) { this.config.parts[part.name] = part // Add part-level config to config - this.config = addPartConfig(part, this.config, this.store) - } else this.store.log.error(`Part must have a name`) - } else this.store.log.error(`Part must have a draft() method`) + this.config = addPartConfig(part, this.config, this.stores[0]) + } else this.stores[0].log.error(`Part must have a name`) + } else this.stores[0].log.error(`Part must have a draft() method`) + + return this +} + +Pattern.prototype.__loadAbsoluteOptionsSet = function (set) { + for (const optionName in this.settings[set].options) { + const option = this.config.options[optionName] + if ( + typeof option !== 'undefined' && + typeof option.snap !== 'undefined' && + option.toAbs instanceof Function + ) { + this.settings[set].absoluteOptions[optionName] = this.__snappedPercentageOption( + optionName, + set + ) + this.stores[set].log.debug( + `🧲 Snapped __${optionName}__ to \`${this.settings[set].absoluteOptions[optionName]}\` for set __${set}__` + ) + } + } return this } @@ -216,66 +246,68 @@ Pattern.prototype.draft = function () { // Late-stage initialization this.init() - if (this.is !== 'sample') { - this.is = 'draft' - this.store.log.debug(`Drafting pattern`) - } - // Handle snap for pct options - for (const i in this.settings.options) { - if ( - typeof this.config.options[i] !== 'undefined' && - typeof this.config.options[i].snap !== 'undefined' && - this.config.options[i].toAbs instanceof Function - ) { - this.settings.absoluteOptions[i] = snappedOption(i, this) - } - } + // Iterate over the provided sets of settings (typically just one) + for (const set in this.settings) { + // Set store + this.stores[set].log.debug(`📐 Drafting pattern (set ${set})`) - this.runHooks('preDraft') - for (const partName of this.config.draftOrder) { - // Create parts - this.store.log.debug(`Creating part \`${partName}\``) - this.parts[partName] = this.__createPartWithContext(partName) - // Handle inject/inheritance - if (typeof this.__inject[partName] === 'string') { - this.store.log.debug(`Creating part \`${partName}\` from part \`${this.__inject[partName]}\``) - try { - this.parts[partName].inject(this.parts[this.__inject[partName]]) - } catch (err) { - this.store.log.error([ - `Could not inject part \`${this.inject[partName]}\` into part \`${partName}\``, - err, - ]) - } - } - if (this.needs(partName)) { - // Draft part - if (typeof this.__parts?.[partName]?.draft === 'function') { + // Create parts container + this.parts[set] = {} + + // Handle snap for pct options + this.__loadAbsoluteOptionsSet(set) + + this.runHooks('preDraft') + for (const partName of this.config.draftOrder) { + // Create parts + this.stores[set].log.debug(`📦 Creating part \`${partName}\` (set ${set})`) + this.parts[set][partName] = this.__createPartWithContext(partName, set) + // Handle inject/inheritance + if (typeof this.__inject[partName] === 'string') { + this.stores[set].log.debug( + `Creating part \`${partName}\` from part \`${this.__inject[partName]}\`` + ) try { - const result = this.__parts[partName].draft(this.parts[partName].shorthand()) - if (typeof result === 'undefined') { - this.store.log.error( - `Result of drafting part ${partName} was undefined. Did you forget to return the part?` - ) - } else this.parts[partName] = result + this.parts[set][partName].inject(this.parts[set][this.__inject[partName]]) } catch (err) { - this.store.log.error([`Unable to draft part \`${partName}\``, err]) + this.stores[set].log.error([ + `Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``, + err, + ]) } - } else this.store.log.error(`Unable to draft pattern. Part.draft() is not callable`) - try { - this.parts[partName].render = - this.parts[partName].render === false ? false : this.wants(partName) - } catch (err) { - this.store.log.error([`Unable to set \`render\` property on part \`${partName}\``, err]) } - } else { - this.store.log.debug( - `Part \`${partName}\` is not needed. Skipping draft and setting render to \`false\`` - ) - this.parts[partName].render = false + if (this.needs(partName, set)) { + // Draft part + if (typeof this.__parts?.[partName]?.draft === 'function') { + try { + const result = this.__parts[partName].draft(this.parts[set][partName].shorthand()) + if (typeof result === 'undefined') { + this.stores[set].log.error( + `Result of drafting part ${partName} was undefined. Did you forget to return the part?` + ) + } else this.parts[set][partName] = result + } catch (err) { + this.stores[set].log.error([`Unable to draft part \`${partName}\` (set ${set})`, err]) + } + } else this.stores[set].log.error(`Unable to draft pattern. Part.draft() is not callable`) + try { + this.parts[set][partName].render = + this.parts[set][partName].render === false ? false : this.wants(partName, set) + } catch (err) { + this.stores[set].log.error([ + `Unable to set \`render\` property on part \`${partName}\``, + err, + ]) + } + } else { + this.stores[set].log.debug( + `Part \`${partName}\` is not needed. Skipping draft and setting render to \`false\`` + ) + this.parts[set][partName].render = false + } } + this.runHooks('postDraft') } - this.runHooks('postDraft') return this } @@ -284,28 +316,26 @@ Pattern.prototype.draft = function () { * Handles pattern sampling */ Pattern.prototype.sample = function () { - // Late-stage initialization - this.init() - if (this.settings.sample.type === 'option') { - return this.sampleOption(this.settings.sample.option) - } else if (this.settings.sample.type === 'measurement') { - return this.sampleMeasurement(this.settings.sample.measurement) + if (this.settings[0].sample.type === 'option') { + return this.sampleOption(this.settings[0].sample.option) + } else if (this.settings[0].sample.type === 'measurement') { + return this.sampleMeasurement(this.settings[0].sample.measurement) } else if (this.settings.sample.type === 'models') { - return this.sampleModels(this.settings.sample.models, this.settings.sample.focus || false) + return this.sampleModels(this.settings[0].sample.models, this.settings[0].sample.focus || false) } } -Pattern.prototype.sampleParts = function () { - let parts = {} - this.settings.complete = false - this.settings.paperless = false - this.draft() - for (let i in this.parts) { - parts[i] = new this.Part() - parts[i].render = this.parts[i].render - } - return parts -} +//Pattern.prototype.sampleParts = function () { +// let parts = {} +// this.settings.complete = false +// this.settings.paperless = false +// this.draft() +// for (let i in this.parts) { +// parts[i] = new this.Part() +// parts[i].render = this.parts[i].render +// } +// return parts +//} Pattern.prototype.sampleRun = function (parts, anchors, run, runs, extraClass = false) { this.draft() @@ -345,109 +375,175 @@ Pattern.prototype.sampleRun = function (parts, anchors, run, runs, extraClass = } } -/** - * Handles option sampling - */ -Pattern.prototype.sampleOption = function (optionName) { - this.is = 'sample' - this.store.log.debug(`Sampling option \`${optionName}\``) - this.runHooks('preSample') - let step, val - let factor = 1 - let anchors = {} - let parts = this.sampleParts() - let option = this.config.options[optionName] - if (typeof option.list === 'object') { - return this.sampleListOption(optionName) +Pattern.prototype.__setBase = function () { + return { + ...this.settings[0], + measurements: { ...(this.settings[0].measurements || {}) }, + options: { ...(this.settings[0].options || {}) }, } +} + +Pattern.prototype.__listOptionSets = function (optionName) { + let option = this.config.options[optionName] + const base = this.__setBase() + const sets = [] + let run = 1 + for (const choice of option.list) { + const settings = { + ...base, + options: { + ...base.options, + }, + idPrefix: `sample-${run}`, + partClasses: `sample-${run}`, + } + settings.options[optionName] = choice + sets.push(settings) + run++ + } + + return sets +} + +Pattern.prototype.__optionSets = function (optionName) { + let option = this.config.options[optionName] + if (typeof option.list === 'object') return this.__listOptionSets(optionName) + const sets = [] + let factor = 1 + let step, val if (typeof option.min === 'undefined' || typeof option.max === 'undefined') { - let min = option * 0.9 - let max = option * 1.1 + const min = option * 0.9 + const max = option * 1.1 option = { min, max } } if (typeof option.pct !== 'undefined') factor = 100 val = option.min / factor step = (option.max / factor - val) / 9 + const base = this.__setBase() for (let run = 1; run < 11; run++) { - this.settings.options[optionName] = val - this.sampleRun(parts, anchors, run, 10) + const settings = { + ...base, + options: { + ...base.options, + }, + idPrefix: `sample-${run}`, + partClasses: `sample-${run}`, + } + settings.options[optionName] = val + sets.push(settings) val += step } - this.parts = parts - this.runHooks('postSample') - return this + return sets } -Pattern.prototype.sampleListOption = function (optionName) { - let parts = this.sampleParts() - let option = this.config.options[optionName] - let anchors = {} - let run = 1 - let runs = option.list.length - for (let val of option.list) { - this.settings.options[optionName] = val - this.sampleRun(parts, anchors, run, runs) - run++ - } - this.parts = parts +/** + * Handles option sampling + */ +Pattern.prototype.sampleOption = function (optionName) { + this.stores[0].log.debug(`Sampling option \`${optionName}\``) + this.runHooks('preSample') + this.__applySettings(this.__optionSets(optionName)) + this.init() + this.runHooks('postSample') - return this + return this.draft() +} + +//Pattern.prototype.sampleListOption = function (optionName) { +// let parts = this.sampleParts() +// let option = this.config.options[optionName] +// let anchors = {} +// let run = 1 +// let runs = option.list.length +// for (let val of option.list) { +// this.settings.options[optionName] = val +// this.sampleRun(parts, anchors, run, runs) +// run++ +// } +// this.parts = parts +// +// return this +//} + +Pattern.prototype.__measurementSets = function (measurementName) { + let val = this.settings[0].measurements[measurementName] + if (val === undefined) + this.stores.log.error( + `Cannot sample measurement \`${measurementName}\` because it's \`undefined\`` + ) + let step = val / 50 + val = val * 0.9 + const sets = [] + const base = this.__setBase() + for (let run = 1; run < 11; run++) { + const settings = { + ...base, + measurements: { + ...base.measurements, + }, + idPrefix: `sample-${run}`, + partClasses: `sample-${run}`, + } + settings.measurements[measurementName] = val + sets.push(settings) + val += step + } + + return sets } /** * Handles measurement sampling */ Pattern.prototype.sampleMeasurement = function (measurementName) { - this.is = 'sample' this.store.log.debug(`Sampling measurement \`${measurementName}\``) this.runHooks('preSample') - let anchors = {} - let parts = this.sampleParts() - let val = this.settings.measurements[measurementName] - if (val === undefined) - this.store.log.error( - `Cannot sample measurement \`${measurementName}\` because it's \`undefined\`` - ) - let step = val / 50 - val = val * 0.9 - for (let run = 1; run < 11; run++) { - this.settings.measurements[measurementName] = val - this.sampleRun(parts, anchors, run, 10) - val += step - } - this.parts = parts + this.__applySettings(this.__measurementSets(measurementName)) + this.init() this.runHooks('postSample') - return this + return this.draft() +} + +Pattern.prototype.__modelSets = function (models, focus = false) { + const sets = [] + const base = this.__setBase() + let run = 1 + // If there's a focus, do it first so it's at the bottom of the SVG + if (focus) { + sets.push({ + ...base, + measurements: models[focus], + idPrefix: `sample-${run}`, + partClasses: `sample-${run} sample-focus`, + }) + run++ + delete models[focus] + } + for (const measurements of Object.values(models)) { + sets.push({ + ...base, + measurements, + idPrefix: `sample-${run}`, + partClasses: `sample-${run}`, + }) + } + + return sets } /** * Handles models sampling */ Pattern.prototype.sampleModels = function (models, focus = false) { - this.is = 'sample' - this.store.log.debug(`Sampling models`) + this.store.log.debug(`Sampling models \`${Object.keys(models).join(', ')}\``) this.runHooks('preSample') - let anchors = {} - let parts = this.sampleParts() - // If there's a focus, do it first so it's at the bottom of the SVG - if (focus) { - this.settings.measurements = models[focus] - this.sampleRun(parts, anchors, -1, -1, 'sample-focus') - delete models[focus] - } - let run = -1 - let runs = Object.keys(models).length - for (let l in models) { - run++ - this.settings.measurements = models[l] - this.sampleRun(parts, anchors, run, runs) - } - this.parts = parts + this.__applySettings(this.__modelSets(models, focus)) + this.init() this.runHooks('postSample') - return this + return this.draft() } Pattern.prototype.render = function () { @@ -478,7 +574,7 @@ Pattern.prototype.__loadPlugin = function (plugin, data) { if (plugin.hooks) this.__loadPluginHooks(plugin, data) if (plugin.macros) this.__loadPluginMacros(plugin) if (plugin.store) this.__loadPluginStoreMethods(plugin) - this.store.log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``) + this.stores[0].log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``) return this } @@ -486,7 +582,7 @@ Pattern.prototype.__loadPlugin = function (plugin, data) { Pattern.prototype.use = function (plugin, data) { if (this.plugins?.[plugin.name]?.condition && !plugin.condition) { // Plugin was first loaded conditionally, and is now loaded explicitly - this.store.log.info( + this.stores[0].log.info( `Plugin \`${plugin.plugin.name} was loaded conditionally earlier, but is now loaded explicitly.` ) return this.__loadPlugin(plugin, data) @@ -497,7 +593,7 @@ Pattern.prototype.use = function (plugin, data) { ? this.__useIf(plugin, data) // Conditional plugin : this.__loadPlugin(plugin, data) // Regular plugin - this.store.log.info( + this.stores[0].log.info( `Plugin \`${ plugin.plugin ? plugin.plugin.name : plugin.name }\` was requested, but it's already loaded. Skipping.` @@ -506,14 +602,18 @@ Pattern.prototype.use = function (plugin, data) { return this } -Pattern.prototype.__useIf = function (plugin, settings) { - if (plugin.condition(settings)) { - this.store.log.info( +Pattern.prototype.__useIf = function (plugin) { + let load = 0 + for (const set of this.settings) { + if (plugin.condition(set)) load++ + } + if (load > 0) { + this.stores[0].log.info( `Condition met: Loaded plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` ) this.__loadPlugin(plugin.plugin, plugin.data) } else { - this.store.log.info( + this.stores[0].log.info( `Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` ) } @@ -542,8 +642,9 @@ Pattern.prototype.__loadPluginMacros = function (plugin) { } Pattern.prototype.__loadPluginStoreMethods = function (plugin) { - if (Array.isArray(plugin.store)) this.store = this.store.extend(...plugin.store) - else this.store.log.warning(`Plugin store methods should be an Array`) + if (Array.isArray(plugin.store)) { + for (const store of this.stores) store.extend(...plugin.store) + } else this.stores[0].log.warning(`Plugin store methods should be an Array`) } Pattern.prototype.macro = function (key, method) { @@ -552,18 +653,23 @@ Pattern.prototype.macro = function (key, method) { /** Packs stacks in a 2D space and sets pattern size */ Pattern.prototype.pack = function () { - if (this.store.logs.error.length > 0) { - this.store.log.warning(`One or more errors occured. Not packing pattern parts`) - return this + for (const set in this.settings) { + if (this.stores[set].logs.error.length > 0) { + this.stores[set].log.warning(`One or more errors occured. Not packing pattern parts`) + return this + } } // First, create all stacks this.stacks = {} - for (const [name, part] of Object.entries(this.parts)) { - const stackName = - typeof part.stack === 'function' ? part.stack(this.settings, name) : part.stack - if (typeof this.stacks[stackName] === 'undefined') - this.stacks[stackName] = this.__createStackWithContext(stackName) - this.stacks[stackName].addPart(part) + for (const set in this.settings) { + for (const [name, part] of Object.entries(this.parts[set])) { + const stackName = + this.settings[set].stackPrefix + + (typeof part.stack === 'function' ? part.stack(this.settings, name) : part.stack) + if (typeof this.stacks[stackName] === 'undefined') + this.stacks[stackName] = this.__createStackWithContext(stackName, set) + this.stacks[stackName].addPart(part) + } } let bins = [] @@ -572,7 +678,7 @@ Pattern.prototype.pack = function () { stack.attributes.remove('transform') if (!this.isStackHidden(key)) { stack.home() - if (this.settings.layout === true) + if (this.settings[0].layout === true) bins.push({ id: key, width: stack.width, height: stack.height }) else { if (this.width < stack.width) this.width = stack.width @@ -580,7 +686,7 @@ Pattern.prototype.pack = function () { } } } - if (this.settings.layout === true) { + if (this.settings[0].layout === true) { let size = pack(bins, { inPlace: true }) for (let bin of bins) { this.autoLayout.stacks[bin.id] = { move: {} } @@ -595,10 +701,10 @@ Pattern.prototype.pack = function () { } this.width = size.width this.height = size.height - } else if (typeof this.settings.layout === 'object') { - this.width = this.settings.layout.width - this.height = this.settings.layout.height - for (let stackId of Object.keys(this.settings.layout.stacks)) { + } else if (typeof this.settings[0].layout === 'object') { + this.width = this.settings[0].layout.width + this.height = this.settings[0].layout.height + for (let stackId of Object.keys(this.settings[0].layout.stacks)) { // Some parts are added by late-stage plugins if (this.stacks[stackId]) { let transforms = this.settings.layout.stacks[stackId] @@ -657,7 +763,7 @@ Pattern.prototype.resolveDependency = function (seen, part, graph = this.depende Pattern.prototype.__addDependency = function (name, part, dep) { this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name]) if (typeof this.__parts[dep.name] === 'undefined') { - this.config = addPartConfig(this.__parts[dep.name], this.config, this.store) + this.config = addPartConfig(this.__parts[dep.name], this.config, this.stores[0]) } return this @@ -673,12 +779,17 @@ Pattern.prototype.__filterOptionalMeasurements = function () { } /** Pre-Resolves part dependencies that are passed in 2022 style */ -Pattern.prototype.__resolveParts = function (count = 0) { +Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { if (count === 0) { for (const part of this.config.parts) { + part.distance = distance this.__parts[part.name] = part } } + distance++ + for (const part of this.config.parts) { + if (typeof part.distance === 'undefined') part.distance = distance + } for (const [name, part] of Object.entries(this.__parts)) { // Hide when hideAll is set if (part.hideAll) part.hide = true @@ -687,6 +798,7 @@ Pattern.prototype.__resolveParts = function (count = 0) { if (part.hideDependencies || part.hideAll) { part.from.hide = true part.from.hideAll = true + part.from.distance = distance } this.__parts[part.from.name] = part.from this.__inject[name] = part.from.name @@ -695,11 +807,13 @@ Pattern.prototype.__resolveParts = function (count = 0) { if (part.after) { if (Array.isArray(part.after)) { for (const dep of part.after) { + dep.distance = distance this.__parts[dep.name] = dep this.__addDependency(name, part, dep) } } else { if (part.hideDependencies) part.after.hide = true + part.after.distance = distance this.__parts[part.after.name] = part.after this.__addDependency(name, part, part.after) } @@ -708,10 +822,10 @@ Pattern.prototype.__resolveParts = function (count = 0) { // Did we discover any new dependencies? const len = Object.keys(this.__parts).length // If so, resolve recursively - if (len > count) return this.__resolveParts(len) + if (len > count) return this.__resolveParts(len, distance) for (const part of Object.values(this.__parts)) { - this.config = addPartConfig(part, this.config, this.store) + this.config = addPartConfig(part, this.config, this.stores[0]) } return this @@ -730,7 +844,7 @@ Pattern.prototype.__resolveDependencies = function (graph = false) { 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') + this.stores[0].log.error('Part dependencies should be a string or an array of strings') throw new Error('Part dependencies should be a string or an array of strings') } } @@ -751,17 +865,20 @@ Pattern.prototype.__resolveDependencies = function (graph = false) { * This depends on the 'only' setting and the * configured dependencies. */ -Pattern.prototype.needs = function (partName) { +Pattern.prototype.needs = function (partName, set = 0) { // If only is unset, all parts are needed if ( - typeof this.settings.only === 'undefined' || - this.settings.only === false || - (Array.isArray(this.settings.only) && this.settings.only.length === 0) + typeof this.settings[set].only === 'undefined' || + this.settings[set].only === false || + (Array.isArray(this.settings[set].only) && this.settings[set].only.length === 0) ) return true // Make only to always be an array - const only = typeof this.settings.only === 'string' ? [this.settings.only] : this.settings.only + const only = + typeof this.settings[set].only === 'string' + ? [this.settings[set].only] + : this.settings[set].only // Walk the only parts, checking each one for a match in its dependencies for (const part of only) { @@ -779,12 +896,12 @@ Pattern.prototype.needs = function (partName) { /** Determines whether a part is wanted by the user * This depends on the 'only' setting */ -Pattern.prototype.wants = function (partName) { +Pattern.prototype.wants = function (partName, set = 0) { // Hidden parts are not wanted if (this.isHidden(partName)) return false - else if (typeof this.settings.only === 'string') return this.settings.only === partName - else if (Array.isArray(this.settings.only)) { - for (const part of this.settings.only) { + else if (typeof this.settings[set].only === 'string') return this.settings[set].only === partName + else if (Array.isArray(this.settings[set].only)) { + for (const part of this.settings[set].only) { if (part === partName) return true } return false @@ -838,12 +955,12 @@ Pattern.prototype.getRenderProps = function () { props.height = this.height props.autoLayout = this.autoLayout props.settings = this.settings - props.logs = { - debug: this.store.logs.debug, - info: this.store.logs.info, - error: this.store.logs.error, - warning: this.store.logs.warning, - } + props.logs = this.stores.map((store) => ({ + debug: store.logs.debug, + info: store.logs.info, + error: store.logs.error, + warning: store.logs.warning, + })) props.parts = {} for (let p in this.parts) { if (this.parts[p].render) { @@ -856,6 +973,7 @@ Pattern.prototype.getRenderProps = function () { width: this.parts[p].width, bottomRight: this.parts[p].bottomRight, topLeft: this.parts[p].topLeft, + store: this.stores[this.parts[p].set], } } } @@ -870,8 +988,14 @@ Pattern.prototype.getRenderProps = function () { } // Merges settings object with default settings -Pattern.prototype.__applySettings = function (settings) { - this.settings = { ...loadPatternDefaults(), ...settings } +Pattern.prototype.__applySettings = function (sets) { + if (!Array.isArray(sets)) throw 'Sets should be an array of settings objects' + if (sets.length === 0) sets.push({}) // Required to load default settings + this.settings = [] + for (const set in sets) { + this.settings.push({ ...loadPatternDefaults(), ...sets[set] }) + if (set > 0) this.stores.push(new Store()) + } return this } diff --git a/packages/core/src/stack.mjs b/packages/core/src/stack.mjs index 894e2c86c78..310b55d5230 100644 --- a/packages/core/src/stack.mjs +++ b/packages/core/src/stack.mjs @@ -76,8 +76,11 @@ Stack.prototype.home = function () { if (this.bottomRight.y === -Infinity) this.bottomRight.y = 0 // Add margin - let margin = this.context.settings.margin - if (this.context.settings.paperless && margin < 10) margin = 10 + let margin = 0 + for (const set in this.context.settings) { + if (this.context.settings[set].margin > margin) margin = this.context.settings[set].margin + if (this.context.settings[set].paperless && margin < 10) margin = 10 + } this.topLeft.x -= margin this.topLeft.y -= margin this.bottomRight.x += margin @@ -90,7 +93,9 @@ Stack.prototype.home = function () { this.height = this.bottomRight.y - this.topLeft.y // Add transform - this.anchor = this.getAnchor() + //this.anchor = this.getAnchor() + // FIXME: Can we be certain this is always (0,0) / + this.anchor = new Point(0, 0) if (this.topLeft.x === this.anchor.x && this.topLeft.y === this.anchor.y) return this else { diff --git a/packages/core/src/utils.mjs b/packages/core/src/utils.mjs index 824dcc52842..ec87269e530 100644 --- a/packages/core/src/utils.mjs +++ b/packages/core/src/utils.mjs @@ -15,7 +15,7 @@ export function isCoord(value) { /** Returns internal hook name for a macro */ export function macroName(name) { - return `_macro_${name}` + return `__macro_${name}` } /** Find intersection of two (endless) lines */ @@ -441,8 +441,14 @@ export function addNonEnumProp(obj, name, value) { const addPartOptions = (part, config, store) => { if (part.options) { for (const optionName in part.options) { - store.log.debug(`Config resolver: Option __${optionName}__ in ${part.name}`) - config.options[optionName] = part.options[optionName] + if (!config.optionDistance[optionName]) { + config.optionDistance[optionName] = part.distance + config.options[optionName] = part.options[optionName] + store.log.debug(`🔵 __${optionName}__ option loaded from \`${part.name}\``) + } else if (config.optionDistance[optionName] > part.distance) { + config.options[optionName] = part.options[optionName] + store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``) + } } } if (part.from) addPartOptions(part.from, config, store) @@ -460,8 +466,10 @@ const addPartMeasurements = (part, config, store, list = false) => { if (!list) list = config.measurements ? [...config.measurements] : [] if (part.measurements) { for (const m of part.measurements) { - list.push(m) - store.log.debug(`Config resolver: Measurement __${m}__ is required in ${part.name}`) + if (list.indexOf(m) === -1) { + list.push(m) + store.log.debug(`🟠 __${m}__ measurement is required in \`${part.name}\``) + } } } if (part.from) addPartMeasurements(part.from, config, store, list) @@ -484,8 +492,10 @@ const addPartOptionalMeasurements = (part, config, store, list = false) => { for (const m of part.optionalMeasurements) { // Don't add it's a required measurement for another part if (config.measurements.indexOf(m) === -1) { - store.log.debug(`Config resolver: Measurement __${m}__ is optional in ${part.name}`) - list.push(m) + if (list.indexOf(m) === -1) { + list.push(m) + store.log.debug(`🟡 __${m}__ measurement is optional in \`${part.name}\``) + } } } } @@ -514,7 +524,7 @@ export const addPartPlugins = (part, config, store) => { const pluginObj = { ...plugin[0], data: plugin[1] } plugin = pluginObj } - store.log.debug(`Config resolver: Plugin __${plugin.name}__ in ${part.name}`) + store.log.debug(`🔌 __${plugin.name}__ plugin in \`${part.name}\``) // Do not overwrite an existing plugin with a conditional plugin unless it is also conditional if (plugin.plugin && plugin.condition) { if (plugins[plugin.plugin.name]?.condition) { @@ -538,7 +548,9 @@ export const addPartPlugins = (part, config, store) => { } export const addPartConfig = (part, config, store) => { + if (part.resolved) return config // Add parts, using set to keep them unique in the array + part.resolved = true config.parts = [...new Set(config.parts).add(part)] config = addPartOptions(part, config, store) config = addPartMeasurements(part, config, store) From e163ed1782ffdb9026e374da92eb91af6a051fed Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Sat, 17 Sep 2022 10:28:01 +0200 Subject: [PATCH 2/4] feat(core): Tests for multisets --- packages/core/tests/multi.test.mjs | 80 +++ packages/core/tests/part.test.mjs | 148 ++--- packages/core/tests/path.test.mjs | 656 ++++++++-------------- packages/core/tests/pattern-init.test.mjs | 261 ++++----- packages/core/tests/snap.test.mjs | 29 +- packages/core/tests/store.test.mjs | 8 +- packages/core/tests/utils.test.mjs | 2 +- 7 files changed, 559 insertions(+), 625 deletions(-) create mode 100644 packages/core/tests/multi.test.mjs diff --git a/packages/core/tests/multi.test.mjs b/packages/core/tests/multi.test.mjs new file mode 100644 index 00000000000..d305eaa5099 --- /dev/null +++ b/packages/core/tests/multi.test.mjs @@ -0,0 +1,80 @@ +import chai from 'chai' +import { Design } from '../src/index.mjs' + +const expect = chai.expect + +if (!expect) console.log('shut up eslint REMOVE') + +describe('Multisets', () => { + describe('FIXME', () => { + const partA = { + name: 'test.partA', + measurements: ['head'], + options: { + size: { pct: 40, min: 20, max: 80 }, + }, + draft: ({ points, Point, paths, Path, part, store, measurements, options }) => { + store.set('size', measurements.head * options.size) + points.from = new Point(0, 0) + points.to = new Point(0, store.get('size')) + paths.line = new Path().move(points.from).line(points.to) + return part + }, + //stack: 'box', + } + const partB = { + name: 'test.partB', + measurements: ['head'], + after: partA, + draft: ({ points, Point, paths, Path, part, store }) => { + points.from = new Point(0, store.get('size')) + points.to = new Point(store.get('size'), store.get('size')) + paths.line = new Path().move(points.from).line(points.to) + return part + }, + //stack: 'box', + } + const partC = { + name: 'test.partC', + after: partB, + draft: ({ points, Point, paths, Path, part, store }) => { + points.from = new Point(store.get('size'), store.get('size')) + points.to = new Point(store.get('size'), 0) + paths.line = new Path().move(points.from).line(points.to) + return part + }, + //stack: 'box', + } + const partD = { + name: 'test.partD', + after: partC, + draft: ({ points, Point, paths, Path, part, store }) => { + points.from = new Point(store.get('size'), 0) + points.to = new Point(0, 0) + paths.line = new Path().move(points.from).line(points.to) + return part + }, + // stack: 'box', + } + + const Pattern = new Design({ + data: { + name: 'test', + version: '1.2.3', + }, + parts: [partD], + }) + const pattern = new Pattern([ + { + measurements: { head: 400 }, + }, + { + measurements: { head: 400 }, + }, + ]) + pattern.draft() + console.log(pattern) + console.log(pattern.render()) + //pattern.render() + }) +}) diff --git a/packages/core/tests/part.test.mjs b/packages/core/tests/part.test.mjs index d3350ee866b..245a1fe6155 100644 --- a/packages/core/tests/part.test.mjs +++ b/packages/core/tests/part.test.mjs @@ -1,5 +1,5 @@ import chai from 'chai' -import { Design, Pattern } from '../src/index.mjs' +import { Design, Part } from '../src/index.mjs' const expect = chai.expect @@ -21,20 +21,24 @@ describe('Part', () => { }) it('Should return a function from macroClosure', () => { - const pattern = new Pattern() - const part = pattern.__createPartWithContext() + const part = new Part() expect(typeof part.macroClosure()).to.equal('function') }) it('Should not run an unknown macro', () => { - const pattern = new Pattern() - const part = pattern.__createPartWithContext() + const part = new Part() const macro = part.macroClosure() expect(macro('unknown')).to.equal(undefined) }) it('Should register and run a macro', () => { - const pattern = new Pattern() + const part = { + name: 'test', + draft: ({ part, macro }) => { + macro('test', { x: 123, y: 456 }) + return part + }, + } const plugin = { name: 'test', version: '0.1-test', @@ -45,39 +49,34 @@ describe('Part', () => { }, }, } - pattern.use(plugin) - const part = pattern.__createPartWithContext() - const macro = part.macroClosure() - macro('test', { x: 123, y: 456 }) - expect(part.points.macro.x).to.equal(123) - expect(part.points.macro.y).to.equal(456) + const design = new Design({ parts: [part], plugins: [plugin] }) + const pattern = new design() + pattern.draft() + expect(pattern.parts[0].test.points.macro.x).to.equal(123) + expect(pattern.parts[0].test.points.macro.y).to.equal(456) }) it('Should return a free ID', () => { - const pattern = new Pattern() - const part = pattern.__createPartWithContext() + const part = new Part() const free = part.getId() expect(part.getId()).to.equal('' + (parseInt(free) + 1)) }) it('Should return a function from unitsClosure', () => { - const pattern = new Pattern() - const part = pattern.__createPartWithContext() + const part = new Part() expect(typeof part.unitsClosure()).to.equal('function') }) it('Should convert units', () => { - const design = new Design() - const pattern = new design() - const part = pattern.__createPartWithContext() + const part = new Part() + part.context = { settings: { units: 'metric' } } const units = part.unitsClosure() expect(units(123.456)).to.equal('12.35cm') - expect(part.units(123.456)).to.equal('12.35cm') + expect(units(123.456)).to.equal('12.35cm') }) it('Should set part attributes', () => { - const pattern = new Pattern() - const part = pattern.__createPartWithContext() + const part = new Part() part.attr('foo', 'bar') expect(part.attributes.get('foo')).to.equal('bar') part.attr('foo', 'baz') @@ -87,60 +86,72 @@ describe('Part', () => { }) it('Should raise a warning when setting a non-Point value in points', () => { - const design = new Design() + const part = { + name: 'test', + draft: ({ points, part }) => { + points.a = 'banana' + return part + }, + } + const design = new Design({ parts: [part] }) const pattern = new design() - const part = pattern.__createPartWithContext() - pattern.init() - const { points } = part.shorthand() - points.a = 'banana' - expect(pattern.store.logs.warning.length).to.equal(4) - expect(pattern.store.logs.warning[0]).to.equal( + pattern.draft() + expect(pattern.stores[0].logs.warning.length).to.equal(4) + expect(pattern.stores[0].logs.warning[0]).to.equal( '`points.a` was set with a value that is not a `Point` object' ) - expect(pattern.store.logs.warning[1]).to.equal( + expect(pattern.stores[0].logs.warning[1]).to.equal( '`points.a` was set with a `x` parameter that is not a `number`' ) - expect(pattern.store.logs.warning[2]).to.equal( + expect(pattern.stores[0].logs.warning[2]).to.equal( '`points.a` was set with a `y` parameter that is not a `number`' ) }) it('Should raise a warning when setting a non-Snippet value in snippets', () => { - const design = new Design() + const part = { + name: 'test', + draft: ({ snippets, part }) => { + snippets.a = 'banana' + return part + }, + } + const design = new Design({ parts: [part] }) const pattern = new design() - const part = pattern.__createPartWithContext() - pattern.init() - const { snippets } = part.shorthand() - snippets.a = 'banana' - expect(pattern.store.logs.warning.length).to.equal(4) - expect(pattern.store.logs.warning[0]).to.equal( + pattern.draft() + expect(pattern.stores[0].logs.warning.length).to.equal(4) + expect(pattern.stores[0].logs.warning[0]).to.equal( '`snippets.a` was set with a value that is not a `Snippet` object' ) - expect(pattern.store.logs.warning[1]).to.equal( + expect(pattern.stores[0].logs.warning[1]).to.equal( '`snippets.a` was set with a `def` parameter that is not a `string`' ) - expect(pattern.store.logs.warning[2]).to.equal( + expect(pattern.stores[0].logs.warning[2]).to.equal( '`snippets.a` was set with an `anchor` parameter that is not a `Point`' ) }) it('Should calculate the part boundary', () => { - const design = new Design() + const part = { + name: 'test', + draft: ({ points, Point, paths, Path, part }) => { + points.from = new Point(123, 456) + points.to = new Point(19, 76) + paths.test = new Path().move(points.from).line(points.to) + return part + }, + } + const design = new Design({ parts: [part] }) const pattern = new design() - const part = pattern.__createPartWithContext() - pattern.init() - const short = part.shorthand() - part.points.from = new short.Point(123, 456) - part.points.to = new short.Point(19, 76) - part.paths.test = new short.Path().move(part.points.from).line(part.points.to) - let boundary = part.boundary() - expect(boundary.topLeft.x).to.equal(19) - expect(boundary.topLeft.y).to.equal(76) - expect(boundary.bottomRight.x).to.equal(123) - expect(boundary.bottomRight.y).to.equal(456) - boundary = part.boundary() - expect(boundary.width).to.equal(104) - expect(boundary.height).to.equal(380) + pattern.draft() + const boundary = pattern.parts[0].test.boundary() + const { topLeft, bottomRight, width, height } = boundary + expect(topLeft.x).to.equal(19) + expect(topLeft.y).to.equal(76) + expect(bottomRight.x).to.equal(123) + expect(bottomRight.y).to.equal(456) + expect(width).to.equal(104) + expect(height).to.equal(380) }) /* @@ -176,7 +187,6 @@ describe('Part', () => { part.home() expect(part.attributes.get('transform')).to.equal(false) }) -*/ it('Should run hooks', () => { let count = 0 const design = new Design() @@ -192,20 +202,25 @@ describe('Part', () => { part.runHooks('preDraft') expect(count).to.equal(1) }) +*/ - it('Should get the units closure to raise a debug when passing a non-number', () => { - const design = new Design() - const pattern = new design({ margin: 5 }) - const part = pattern.__createPartWithContext() - pattern.init() - const short = part.shorthand() - short.units('a') - expect(pattern.store.logs.warning.length).to.equal(1) - expect(pattern.store.logs.warning[0]).to.equal( + it('Units closure should log a warning when passing a non-number', () => { + const part = { + name: 'test', + draft: ({ units, part }) => { + units('a') + return part + }, + } + const design = new Design({ parts: [part] }) + const pattern = new design() + pattern.draft() + expect(pattern.stores[0].logs.warning.length).to.equal(1) + expect(pattern.stores[0].logs.warning[0]).to.equal( 'Calling `units(value)` but `value` is not a number (`string`)' ) }) - + /* describe('isEmpty', () => { it('Should return true if the part has no paths or snippets', () => { const design = new Design() @@ -272,4 +287,5 @@ describe('Part', () => { expect(part.isEmpty()).to.be.false }) }) + */ }) diff --git a/packages/core/tests/path.test.mjs b/packages/core/tests/path.test.mjs index 9bd5ba15529..a703f6161fc 100644 --- a/packages/core/tests/path.test.mjs +++ b/packages/core/tests/path.test.mjs @@ -1,5 +1,5 @@ import chai from 'chai' -import { round, Pattern, Path, Point, Design } from '../src/index.mjs' +import { round, Path, Point, Design } from '../src/index.mjs' const expect = chai.expect @@ -16,8 +16,8 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft().render() - expect(pattern.parts.test.paths.offset.bottomRight.x).to.equal(-10) - expect(pattern.parts.test.paths.offset.bottomRight.y).to.equal(40) + expect(pattern.parts[0].test.paths.offset.bottomRight.x).to.equal(-10) + expect(pattern.parts[0].test.paths.offset.bottomRight.y).to.equal(40) }) it('Should offset a curve', () => { @@ -34,8 +34,8 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft().render() - expect(round(pattern.parts.test.paths.offset.bottomRight.x)).to.equal(72.18) - expect(round(pattern.parts.test.paths.offset.bottomRight.y)).to.equal(38.26) + expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(72.18) + expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(38.26) }) it('Should offset a curve where cp1 = start', () => { @@ -50,8 +50,8 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft().render() - expect(round(pattern.parts.test.paths.offset.bottomRight.x)).to.equal(72.63) - expect(round(pattern.parts.test.paths.offset.bottomRight.y)).to.equal(26.48) + expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(72.63) + expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(26.48) }) it('Should offset a curve where cp2 = end', () => { @@ -66,8 +66,8 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft().render() - expect(round(pattern.parts.test.paths.offset.bottomRight.x)).to.equal(119.26) - expect(round(pattern.parts.test.paths.offset.bottomRight.y)).to.equal(43.27) + expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(119.26) + expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(43.27) }) /* @@ -101,7 +101,7 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft().render() - expect(pattern.parts.test.paths.line.length()).to.equal(40) + expect(pattern.parts[0].test.paths.line.length()).to.equal(40) }) it('Should return the length of a curve', () => { @@ -118,7 +118,7 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft().render() - expect(round(pattern.parts.test.paths.curve.length())).to.equal(145.11) + expect(round(pattern.parts[0].test.paths.curve.length())).to.equal(145.11) }) it('Should return the path start point', () => { @@ -135,8 +135,8 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft().render() - expect(pattern.parts.test.paths.curve.start().x).to.equal(123) - expect(pattern.parts.test.paths.curve.start().y).to.equal(456) + expect(pattern.parts[0].test.paths.curve.start().x).to.equal(123) + expect(pattern.parts[0].test.paths.curve.start().y).to.equal(456) }) it('Should return the path end point', () => { @@ -153,35 +153,26 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft().render() - expect(pattern.parts.test.paths.curve.end().x).to.equal(123) - expect(pattern.parts.test.paths.curve.end().y).to.equal(456) + expect(pattern.parts[0].test.paths.curve.end().x).to.equal(123) + expect(pattern.parts[0].test.paths.curve.end().y).to.equal(456) }) it('Should calculate that path boundary', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.curve = new a.Path() - .move(new a.Point(123, 456)) - .curve(new a.Point(0, 40), new a.Point(123, 34), new a.Point(230, 4)) - a.paths.curve.boundary() - expect(a.paths.curve.topLeft.x).to.equal(71.6413460920667) - expect(a.paths.curve.topLeft.y).to.equal(4) - a.paths.curve.boundary() - expect(a.paths.curve.bottomRight.x).to.equal(230) - expect(a.paths.curve.bottomRight.y).to.equal(456) + const curve = new Path() + .move(new Point(123, 456)) + .curve(new Point(0, 40), new Point(123, 34), new Point(230, 4)) + curve.boundary() + expect(curve.topLeft.x).to.equal(71.6413460920667) + expect(curve.topLeft.y).to.equal(4) + expect(curve.bottomRight.x).to.equal(230) + expect(curve.bottomRight.y).to.equal(456) }) it('Should clone a path', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.curve = new a.Path() - .move(new a.Point(123, 456)) - .curve(new a.Point(0, 40), new a.Point(123, 34), new a.Point(230, 4)) - let b = a.paths.curve.clone() + const curve = new Path() + .move(new Point(123, 456)) + .curve(new Point(0, 40), new Point(123, 34), new Point(230, 4)) + let b = curve.clone() b.boundary() expect(b.topLeft.x).to.equal(71.6413460920667) expect(b.topLeft.y).to.equal(4) @@ -191,129 +182,79 @@ describe('Path', () => { }) it('Should join paths', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.line = new a.Path().move(new a.Point(0, 0)).line(new a.Point(0, 40)) - a.paths.curve = new a.Path() - .move(new a.Point(123, 456)) - .curve(new a.Point(0, 40), new a.Point(123, 34), new a.Point(230, 4)) - a.paths.joint = a.paths.curve.join(a.paths.line) - expect(a.paths.joint.ops.length).to.equal(4) + const line = new Path().move(new Point(0, 0)).line(new Point(0, 40)) + const curve = new Path() + .move(new Point(123, 456)) + .curve(new Point(0, 40), new Point(123, 34), new Point(230, 4)) + const joint = curve.join(line) + expect(joint.ops.length).to.equal(4) }) it('Should throw error when joining a closed paths', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.line = new a.Path().move(new a.Point(0, 0)).line(new a.Point(0, 40)) - a.paths.curve = new a.Path() - .move(new a.Point(123, 456)) - .curve(new a.Point(0, 40), new a.Point(123, 34), new a.Point(230, 4)) + const line = new Path().move(new Point(0, 0)).line(new Point(0, 40)) + const curve = new Path() + .move(new Point(123, 456)) + .curve(new Point(0, 40), new Point(123, 34), new Point(230, 4)) .close() - expect(() => a.paths.curve.join(a.paths.line)).to.throw() + expect(() => curve.join(line)).to.throw() }) it('Should shift along a line', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.line = new a.Path().move(new a.Point(0, 0)).line(new a.Point(0, 40)) - expect(a.paths.line.shiftAlong(20).y).to.equal(20) + const line = new Path().move(new Point(0, 0)).line(new Point(0, 40)) + expect(line.shiftAlong(20).y).to.equal(20) }) it('Should not shift along a path/line if we end up on the end point', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.line = new a.Path().move(new a.Point(0, 0)).line(new a.Point(10, 0)) - expect(a.paths.line.shiftAlong(10).x).to.equal(10) + const line = new Path().move(new Point(0, 0)).line(new Point(10, 0)) + expect(line.shiftAlong(10).x).to.equal(10) }) it('Should shift along lines', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.line = new a.Path() - .move(new a.Point(0, 0)) - .line(new a.Point(0, 40)) - .line(new a.Point(100, 40)) - expect(a.paths.line.shiftAlong(50).x).to.equal(10) - expect(a.paths.line.shiftAlong(50).y).to.equal(40) + const line = new Path().move(new Point(0, 0)).line(new Point(0, 40)).line(new Point(100, 40)) + expect(line.shiftAlong(50).x).to.equal(10) + expect(line.shiftAlong(50).y).to.equal(40) }) it('Should shift along curve + line', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.test = new a.Path() - .move(new a.Point(0, 0)) - .line(new a.Point(0, 40)) - .curve(new a.Point(40, 40), new a.Point(40, 0), new a.Point(200, 0)) - .line(new a.Point(200, 400)) - expect(round(a.paths.test.shiftAlong(500).x)).to.equal(200) - expect(round(a.paths.test.shiftAlong(500).y)).to.equal(253.74) + const test = new Path() + .move(new Point(0, 0)) + .line(new Point(0, 40)) + .curve(new Point(40, 40), new Point(40, 0), new Point(200, 0)) + .line(new Point(200, 400)) + expect(round(test.shiftAlong(500).x)).to.equal(200) + expect(round(test.shiftAlong(500).y)).to.equal(253.74) }) - it("Should throw error when shifting along path further than it's long", () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.paths.test = new a.Path() - .move(new a.Point(0, 0)) - .line(new a.Point(0, 40)) - .line(new a.Point(200, 400)) - expect(() => a.paths.test.shiftAlong(500)).to.throw() + it("Should throw an error when shifting along path further than it's long", () => { + const test = new Path().move(new Point(0, 0)).line(new Point(0, 40)).line(new Point(200, 400)) + expect(() => test.shiftAlong(500)).to.throw() }) it('Should shift along with sufficient precision', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.paths.test = new a.Path() - .move(new a.Point(0, 0)) - .curve(new a.Point(123, 123), new a.Point(-123, 456), new a.Point(456, -123)) - a.points.a = a.paths.test.shiftAlong(100) - a.points.b = a.paths.test.reverse().shiftAlong(a.paths.test.length() - 100) - expect(a.points.a.dist(a.points.b)).to.below(0.05) + const test = new Path() + .move(new Point(0, 0)) + .curve(new Point(123, 123), new Point(-123, 456), new Point(456, -123)) + const a = test.shiftAlong(100) + const b = test.reverse().shiftAlong(test.length() - 100) + expect(a.dist(b)).to.below(0.05) }) it('Should shift fraction with sufficient precision', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.paths.test = new a.Path() - .move(new a.Point(0, 0)) - .curve(new a.Point(123, 123), new a.Point(-123, 456), new a.Point(456, -123)) - a.points.a = a.paths.test.shiftFractionAlong(0.5) - a.points.b = a.paths.test.reverse().shiftFractionAlong(0.5) - expect(a.points.a.dist(a.points.b)).to.below(0.05) + const test = new Path() + .move(new Point(0, 0)) + .curve(new Point(123, 123), new Point(-123, 456), new Point(456, -123)) + const a = test.shiftFractionAlong(0.5) + const b = test.reverse().shiftFractionAlong(0.5) + expect(a.dist(b)).to.below(0.05) }) it('Should shift a fraction along a line', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.paths.line = new a.Path() - .move(new a.Point(0, 0)) - .line(new a.Point(0, 40)) - .line(new a.Point(100, 40)) - expect(round(a.paths.line.shiftFractionAlong(0.5).x)).to.equal(30) - expect(round(a.paths.line.shiftFractionAlong(0.5).y)).to.equal(40) + const line = new Path().move(new Point(0, 0)).line(new Point(0, 40)).line(new Point(100, 40)) + expect(round(line.shiftFractionAlong(0.5).x)).to.equal(30) + expect(round(line.shiftFractionAlong(0.5).y)).to.equal(40) }) it('Should find the bounding box of a line', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let Path = pattern.parts.a.Path - let Point = pattern.parts.a.Point - let line = new Path().move(new Point(3, 2)).line(new Point(10, 40)) let box = line.bbox() expect(box.topLeft.x).to.equal(3) @@ -371,15 +312,12 @@ describe('Path', () => { expect(box.bottomRight.y).to.equal(12) }) - it('Should find the bounding box of a line', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.paths.curve = new a.Path() - .move(new a.Point(123, 456)) - .curve(new a.Point(0, 40), new a.Point(123, 34), new a.Point(230, 4)) + it('Should find the bounding box of a curve', () => { + const curve = new Path() + .move(new Point(123, 456)) + .curve(new Point(0, 40), new Point(123, 34), new Point(230, 4)) .close() - let box = a.paths.curve.bbox() + const box = curve.bbox() expect(round(box.topLeft.x)).to.equal(71.64) expect(box.topLeft.y).to.equal(4) expect(box.bottomRight.x).to.equal(230) @@ -387,13 +325,10 @@ describe('Path', () => { }) it('Should reverse a path', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - let test = new a.Path() - .move(new a.Point(123, 456)) - .line(new a.Point(12, 23)) - .curve(new a.Point(0, 40), new a.Point(123, 34), new a.Point(230, 4)) + const test = new Path() + .move(new Point(123, 456)) + .line(new Point(12, 23)) + .curve(new Point(0, 40), new Point(123, 34), new Point(230, 4)) .close() let rev = test.reverse() let tb = test.bbox() @@ -407,98 +342,69 @@ describe('Path', () => { }) it('Should find the edges of a path', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(45, 60) - a.points.B = new a.Point(10, 30) - a.points.BCp2 = new a.Point(40, 20) - a.points.C = new a.Point(90, 30) - a.points.CCp1 = new a.Point(50, -30) - a.points.D = new a.Point(-60, 90) - a.points.E = new a.Point(90, 190) - a.paths.test = new a.Path() - .move(a.points.A) - .line(a.points.B) - .curve(a.points.BCp2, a.points.CCp1, a.points.C) - .curve(a.points.E, a.points.D, a.points.A) + const test = new Path() + .move(new Point(45, 60)) + .line(new Point(10, 30)) + .curve(new Point(40, 20), new Point(50, -30), new Point(90, 30)) + .curve(new Point(90, 190), new Point(-60, 90), new Point(45, 60)) .close() - expect(round(a.paths.test.edge('topLeft').x)).to.equal(7.7) - expect(round(a.paths.test.edge('topLeft').y)).to.equal(0.97) - expect(round(a.paths.test.edge('bottomLeft').x)).to.equal(7.7) - expect(round(a.paths.test.edge('bottomLeft').y)).to.equal(118.46) - expect(round(a.paths.test.edge('bottomRight').x)).to.equal(90) - expect(round(a.paths.test.edge('bottomRight').y)).to.equal(118.46) - expect(round(a.paths.test.edge('topRight').x)).to.equal(90) - expect(round(a.paths.test.edge('topRight').y)).to.equal(0.97) - expect(round(a.paths.test.edge('left').x)).to.equal(7.7) - expect(round(a.paths.test.edge('left').y)).to.equal(91.8) - expect(round(a.paths.test.edge('bottom').x)).to.equal(40.63) - expect(round(a.paths.test.edge('bottom').y)).to.equal(118.46) - expect(round(a.paths.test.edge('right').x)).to.equal(89.76) - expect(round(a.paths.test.edge('right').y)).to.equal(29.64) - expect(round(a.paths.test.edge('top').x)).to.equal(55.98) - expect(round(a.paths.test.edge('top').y)).to.equal(0.97) + expect(round(test.edge('topLeft').x)).to.equal(7.7) + expect(round(test.edge('topLeft').y)).to.equal(0.97) + expect(round(test.edge('bottomLeft').x)).to.equal(7.7) + expect(round(test.edge('bottomLeft').y)).to.equal(118.46) + expect(round(test.edge('bottomRight').x)).to.equal(90) + expect(round(test.edge('bottomRight').y)).to.equal(118.46) + expect(round(test.edge('topRight').x)).to.equal(90) + expect(round(test.edge('topRight').y)).to.equal(0.97) + expect(round(test.edge('left').x)).to.equal(7.7) + expect(round(test.edge('left').y)).to.equal(91.8) + expect(round(test.edge('bottom').x)).to.equal(40.63) + expect(round(test.edge('bottom').y)).to.equal(118.46) + expect(round(test.edge('right').x)).to.equal(89.76) + expect(round(test.edge('right').y)).to.equal(29.64) + expect(round(test.edge('top').x)).to.equal(55.98) + expect(round(test.edge('top').y)).to.equal(0.97) }) it('Should find the edges of a path for corner cases', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(-45, -60) - a.points.B = new a.Point(45, 60) - a.points.C = new a.Point(-90, -160) - a.paths.test = new a.Path().move(a.points.A).line(a.points.B) - expect(round(a.paths.test.edge('top').x)).to.equal(-45) - expect(round(a.paths.test.edge('top').y)).to.equal(-60) - expect(round(a.paths.test.edge('left').x)).to.equal(-45) - expect(round(a.paths.test.edge('left').y)).to.equal(-60) - expect(round(a.paths.test.edge('bottom').x)).to.equal(45) - expect(round(a.paths.test.edge('bottom').y)).to.equal(60) - expect(round(a.paths.test.edge('right').x)).to.equal(45) - expect(round(a.paths.test.edge('right').y)).to.equal(60) - a.paths.test = new a.Path().move(a.points.B).line(a.points.A) - expect(round(a.paths.test.edge('top').x)).to.equal(-45) - expect(round(a.paths.test.edge('top').y)).to.equal(-60) - expect(round(a.paths.test.edge('left').x)).to.equal(-45) - expect(round(a.paths.test.edge('left').y)).to.equal(-60) - expect(round(a.paths.test.edge('bottom').x)).to.equal(45) - expect(round(a.paths.test.edge('bottom').y)).to.equal(60) - expect(round(a.paths.test.edge('right').x)).to.equal(45) - expect(round(a.paths.test.edge('right').y)).to.equal(60) + let test = new Path().move(new Point(-45, -60)).line(new Point(45, 60)) + expect(round(test.edge('top').x)).to.equal(-45) + expect(round(test.edge('top').y)).to.equal(-60) + expect(round(test.edge('left').x)).to.equal(-45) + expect(round(test.edge('left').y)).to.equal(-60) + expect(round(test.edge('bottom').x)).to.equal(45) + expect(round(test.edge('bottom').y)).to.equal(60) + expect(round(test.edge('right').x)).to.equal(45) + expect(round(test.edge('right').y)).to.equal(60) + test = new Path().move(new Point(45, 60)).line(new Point(-45, -60)) + expect(round(test.edge('top').x)).to.equal(-45) + expect(round(test.edge('top').y)).to.equal(-60) + expect(round(test.edge('left').x)).to.equal(-45) + expect(round(test.edge('left').y)).to.equal(-60) + expect(round(test.edge('bottom').x)).to.equal(45) + expect(round(test.edge('bottom').y)).to.equal(60) + expect(round(test.edge('right').x)).to.equal(45) + expect(round(test.edge('right').y)).to.equal(60) }) it('Should find the edge of a path for this edge-case', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(-109.7, 77, 12) - a.points.B = new a.Point(-27.33, 99.19) - a.points.C = new a.Point(-39.45, 137.4) - a.points.D = new a.Point(-61.52, 219.77) - a.paths.test = new a.Path().move(a.points.A).curve(a.points.B, a.points.C, a.points.D) - expect(round(a.paths.test.edge('right').x)).to.equal(-45.22) - expect(round(a.paths.test.edge('right').y)).to.equal(139.4) + const test = new Path() + .move(new Point(-109.7, 77)) + .curve(new Point(-27.33, 99.19), new Point(-39.45, 137.4), new Point(-61.52, 219.77)) + expect(round(test.edge('right').x)).to.equal(-45.22) + expect(round(test.edge('right').y)).to.equal(139.4) }) it('Should find where a path intersects with an X value', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(95, 50) - a.points.B = new a.Point(10, 30) - a.points.BCp2 = new a.Point(40, 20) - a.points.C = new a.Point(90, 30) - a.points.CCp1 = new a.Point(50, -30) - a.points.D = new a.Point(50, 130) - a.points.DCp1 = new a.Point(150, 30) - a.paths.test = new a.Path() - .move(a.points.A) - .line(a.points.B) - .curve(a.points.BCp2, a.points.CCp1, a.points.C) - .curve(a.points.DCp1, a.points.DCp1, a.points.D) - .close() - let intersections = a.paths.test.intersectsX(60) + const A = new Point(95, 50) + const B = new Point(10, 30) + const BCp2 = new Point(40, 20) + const C = new Point(90, 30) + const CCp1 = new Point(50, -30) + const D = new Point(50, 130) + const DCp1 = new Point(150, 30) + const test = new Path().move(A).line(B).curve(BCp2, CCp1, C).curve(DCp1, DCp1, D).close() + const intersections = test.intersectsX(60) expect(intersections.length).to.equal(4) expect(round(intersections[0].x)).to.equal(60) expect(round(intersections[0].y)).to.equal(41.76) @@ -511,23 +417,15 @@ describe('Path', () => { }) it('Should find where a path intersects with an Y value', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(95, 50) - a.points.B = new a.Point(10, 30) - a.points.BCp2 = new a.Point(40, 20) - a.points.C = new a.Point(90, 30) - a.points.CCp1 = new a.Point(50, -30) - a.points.D = new a.Point(50, 130) - a.points.DCp1 = new a.Point(150, 30) - a.paths.test = new a.Path() - .move(a.points.A) - .line(a.points.B) - .curve(a.points.BCp2, a.points.CCp1, a.points.C) - .curve(a.points.DCp1, a.points.DCp1, a.points.D) - .close() - let intersections = a.paths.test.intersectsY(60) + const A = new Point(95, 50) + const B = new Point(10, 30) + const BCp2 = new Point(40, 20) + const C = new Point(90, 30) + const CCp1 = new Point(50, -30) + const D = new Point(50, 130) + const DCp1 = new Point(150, 30) + const test = new Path().move(A).line(B).curve(BCp2, CCp1, C).curve(DCp1, DCp1, D).close() + let intersections = test.intersectsY(60) expect(intersections.length).to.equal(2) expect(round(intersections[0].x)).to.equal(117.83) expect(round(intersections[0].y)).to.equal(60) @@ -536,45 +434,31 @@ describe('Path', () => { }) it('Should throw an error when not passing a value to path.intersectsX', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.paths.test = new a.Path() - expect(() => a.paths.test.intersectsX()).to.throw() - expect(() => a.paths.test.intersectsY()).to.throw() + const test = new Path() + expect(() => test.intersectsX()).to.throw() + expect(() => test.intersectsY()).to.throw() }) it('Should find the intersections between two paths', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(45, 60) - a.points.B = new a.Point(10, 30) - a.points.BCp2 = new a.Point(40, 20) - a.points.C = new a.Point(90, 30) - a.points.CCp1 = new a.Point(50, -30) - a.points.D = new a.Point(50, 130) - a.points.DCp1 = new a.Point(150, 30) + const A = new Point(45, 60) + const B = new Point(10, 30) + const BCp2 = new Point(40, 20) + const C = new Point(90, 30) + const CCp1 = new Point(50, -30) + const D = new Point(50, 130) + const DCp1 = new Point(150, 30) - a.points._A = new a.Point(55, 40) - a.points._B = new a.Point(0, 55) - a.points._BCp2 = new a.Point(40, -20) - a.points._C = new a.Point(90, 40) - a.points._CCp1 = new a.Point(50, -30) - a.points._D = new a.Point(40, 120) - a.points._DCp1 = new a.Point(180, 40) + const _A = new Point(55, 40) + const _B = new Point(0, 55) + const _BCp2 = new Point(40, -20) + const _C = new Point(90, 40) + const _CCp1 = new Point(50, -30) + const _D = new Point(40, 120) + const _DCp1 = new Point(180, 40) - a.paths.example1 = new a.Path() - .move(a.points.A) - .line(a.points.B) - .curve(a.points.BCp2, a.points.CCp1, a.points.C) - .curve(a.points.DCp1, a.points.DCp1, a.points.D) - a.paths.example2 = new a.Path() - .move(a.points._A) - .line(a.points._B) - .curve(a.points._BCp2, a.points._CCp1, a.points._C) - .curve(a.points._DCp1, a.points._DCp1, a.points._D) - let intersections = a.paths.example1.intersects(a.paths.example2) + const example1 = new Path().move(A).line(B).curve(BCp2, CCp1, C).curve(DCp1, DCp1, D) + const example2 = new Path().move(_A).line(_B).curve(_BCp2, _CCp1, _C).curve(_DCp1, _DCp1, _D) + let intersections = example1.intersects(example2) expect(intersections.length).to.equal(6) expect(round(intersections[0].x)).to.equal(29.71) expect(round(intersections[0].y)).to.equal(46.9) @@ -591,31 +475,20 @@ describe('Path', () => { }) it('Should throw an error when running path.intersect on an identical path', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.paths.test = new a.Path() - expect(() => a.paths.test.intersects(a.paths.test)).to.throw() + const test = new Path() + expect(() => test.intersects(test)).to.throw() }) it('Should divide a path', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(45, 60) - a.points.B = new a.Point(10, 30) - a.points.BCp2 = new a.Point(40, 20) - a.points.C = new a.Point(90, 30) - a.points.CCp1 = new a.Point(50, -30) - a.points.D = new a.Point(-60, 90) - a.points.E = new a.Point(90, 190) - a.paths.test = new a.Path() - .move(a.points.A) - .line(a.points.B) - .curve(a.points.BCp2, a.points.CCp1, a.points.C) - .curve(a.points.E, a.points.D, a.points.A) - .close() - let divided = a.paths.test.divide() + const A = new Point(45, 60) + const B = new Point(10, 30) + const BCp2 = new Point(40, 20) + const C = new Point(90, 30) + const CCp1 = new Point(50, -30) + const D = new Point(-60, 90) + const E = new Point(90, 190) + const test = new Path().move(A).line(B).curve(BCp2, CCp1, C).curve(E, D, A).close() + let divided = test.divide() expect(divided.length).to.equal(4) expect(divided[0].ops[0].type).to.equal('move') expect(divided[0].ops[0].to.x).to.equal(45) @@ -652,25 +525,18 @@ describe('Path', () => { }) it('Should split a path on a curve', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(45, 60) - a.points.B = new a.Point(10, 30) - a.points.BCp2 = new a.Point(40, 20) - a.points.C = new a.Point(90, 30) - a.points.CCp1 = new a.Point(50, -30) - a.points.D = new a.Point(50, 130) - a.points.DCp1 = new a.Point(150, 30) + const A = new Point(45, 60) + const B = new Point(10, 30) + const BCp2 = new Point(40, 20) + const C = new Point(90, 30) + const CCp1 = new Point(50, -30) + const D = new Point(50, 130) + const DCp1 = new Point(150, 30) - a.paths.test = new a.Path() - .move(a.points.A) - .line(a.points.B) - .curve(a.points.BCp2, a.points.CCp1, a.points.C) - .curve(a.points.DCp1, a.points.DCp1, a.points.D) + const test = new Path().move(A).line(B).curve(BCp2, CCp1, C).curve(DCp1, DCp1, D) - a.points.split = a.paths.test.shiftAlong(120) - let halves = a.paths.test.split(a.points.split) + const split = test.shiftAlong(120) + let halves = test.split(split) let curve = halves[0].ops.pop() expect(curve.type).to.equal('curve') expect(round(curve.cp1.x)).to.equal(35.08) @@ -682,25 +548,18 @@ describe('Path', () => { }) it('Should split a path on a line', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(45, 60) - a.points.B = new a.Point(10, 30) - a.points.BCp2 = new a.Point(40, 20) - a.points.C = new a.Point(90, 30) - a.points.CCp1 = new a.Point(50, -30) - a.points.D = new a.Point(50, 130) - a.points.DCp1 = new a.Point(150, 30) + const A = new Point(45, 60) + const B = new Point(10, 30) + const BCp2 = new Point(40, 20) + const C = new Point(90, 30) + const CCp1 = new Point(50, -30) + const D = new Point(50, 130) + const DCp1 = new Point(150, 30) - a.paths.test = new a.Path() - .move(a.points.A) - .line(a.points.B) - .curve(a.points.BCp2, a.points.CCp1, a.points.C) - .curve(a.points.DCp1, a.points.DCp1, a.points.D) + const test = new Path().move(A).line(B).curve(BCp2, CCp1, C).curve(DCp1, DCp1, D) - a.points.split = a.paths.test.shiftAlong(20) - let halves = a.paths.test.split(a.points.split) + const split = test.shiftAlong(20) + let halves = test.split(split) let line = halves[0].ops.pop() expect(line.type).to.equal('line') expect(round(line.to.x)).to.equal(29.81) @@ -708,22 +567,12 @@ describe('Path', () => { }) it('Should trim a path when lines overlap', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(0, 0) - a.points.B = new a.Point(100, 100) - a.points.C = new a.Point(0, 100) - a.points.D = new a.Point(100, 0) + const A = new Point(0, 0) + const B = new Point(100, 100) + const C = new Point(0, 100) + const D = new Point(100, 0) - let test = new a.Path() - .move(new a.Point(0, 20)) - .line(a.points.A) - .line(a.points.B) - .line(a.points.C) - .line(a.points.D) - .line(a.points.A) - .trim() + let test = new Path().move(new Point(0, 20)).line(A).line(B).line(C).line(D).line(A).trim() expect(test.ops.length).to.equal(5) expect(test.ops[2].to.x).to.equal(50) @@ -731,21 +580,18 @@ describe('Path', () => { }) it('Should trim a path when a line overlaps with a curve', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(0, 0) - a.points.B = new a.Point(100, 100) - a.points.C = new a.Point(0, 100) - a.points.D = new a.Point(100, 0) + const A = new Point(0, 0) + const B = new Point(100, 100) + const C = new Point(0, 100) + const D = new Point(100, 0) - let test = new a.Path() - .move(new a.Point(0, 20)) - .line(a.points.A) - .curve(a.points.D, a.points.B, a.points.B) - .line(a.points.C) - .line(a.points.D) - .line(a.points.A) + let test = new Path() + .move(new Point(0, 20)) + .line(A) + .curve(D, B, B) + .line(C) + .line(D) + .line(A) .trim() expect(test.ops.length).to.equal(5) @@ -754,21 +600,18 @@ describe('Path', () => { }) it('Should trim a path when a curves overlap', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(0, 0) - a.points.B = new a.Point(100, 100) - a.points.C = new a.Point(0, 100) - a.points.D = new a.Point(100, 0) + const A = new Point(0, 0) + const B = new Point(100, 100) + const C = new Point(0, 100) + const D = new Point(100, 0) - let test = new a.Path() - .move(new a.Point(0, 20)) - .line(a.points.A) - .curve(a.points.D, a.points.B, a.points.B) - .line(a.points.C) - .curve(a.points.C, a.points.A, a.points.D) - .line(a.points.A) + let test = new Path() + .move(new Point(0, 20)) + .line(A) + .curve(D, B, B) + .line(C) + .curve(C, A, D) + .line(A) .trim() expect(test.ops.length).to.equal(5) @@ -777,15 +620,12 @@ describe('Path', () => { }) it('Should translate a path', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - a.points.A = new a.Point(0, 0) - a.points.B = new a.Point(100, 100) - a.points.C = new a.Point(0, 100) - a.points.D = new a.Point(100, 0) + const A = new Point(0, 0) + const B = new Point(100, 100) + const C = new Point(0, 100) + const D = new Point(100, 0) - let base = new a.Path().move(a.points.A).curve(a.points.B, a.points.C, a.points.D) + let base = new Path().move(A).curve(B, C, D) let test = base.translate(10, 20) expect(test.ops.length).to.equal(2) @@ -796,16 +636,12 @@ describe('Path', () => { }) it('Should add a path attribute', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.line = new a.Path() - .move(new a.Point(0, 0)) - .line(new a.Point(0, 40)) + const line = new Path() + .move(new Point(0, 0)) + .line(new Point(0, 40)) .attr('class', 'foo') .attr('class', 'bar') - expect(a.paths.line.attributes.get('class')).to.equal('foo bar') + expect(line.attributes.get('class')).to.equal('foo bar') }) it('Should overwrite a path attribute', () => { @@ -826,27 +662,23 @@ describe('Path', () => { pattern.draft().render() // Paths from shorthand have the log method - expect(pattern.parts.test.paths.line.attributes.get('class')).to.equal('overwritten') + expect(pattern.parts[0].test.paths.line.attributes.get('class')).to.equal('overwritten') }) it('Should move along a path even if it lands just on a joint', () => { - let pattern = new Pattern() - pattern.parts.a = pattern.__createPartWithContext('a') - let a = pattern.parts.a - - a.paths.curve = new a.Path() - .move(new a.Point(20.979322245694167, -219.8547313525503)) + const curve = new Path() + .move(new Point(20.979322245694167, -219.8547313525503)) ._curve( - new a.Point(35.33122482627704, -153.54225517257478), - new a.Point(61.99376179214562, -105.99242252587702) + new Point(35.33122482627704, -153.54225517257478), + new Point(61.99376179214562, -105.99242252587702) ) .curve( - new a.Point(88.85254026593002, -58.092613773317105), - new a.Point(136.13264764576948, -11.692646171119936), - new a.Point(170.69593749999996, -4.180844669736632e-14) + new Point(88.85254026593002, -58.092613773317105), + new Point(136.13264764576948, -11.692646171119936), + new Point(170.69593749999996, -4.180844669736632e-14) ) - a.points.test = a.paths.curve.shiftAlong(121.36690836797631) - expect(a.points.test).to.be.instanceOf(a.Point) + const test = curve.shiftAlong(121.36690836797631) + expect(test).to.be.instanceOf(Point) }) it('Should add log methods to a path', () => { @@ -1083,8 +915,8 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft() - expect(pattern.store.logs.error.length).to.equal(2) - expect(pattern.store.logs.error[0]).to.equal( + expect(pattern.stores[0].logs.error.length).to.equal(2) + expect(pattern.stores[0].logs.error[0]).to.equal( 'Called `Path.offset(distance)` but `distance` is not a number' ) }) @@ -1101,8 +933,8 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft() - expect(pattern.store.logs.error.length).to.equal(2) - expect(pattern.store.logs.error[0]).to.equal( + expect(pattern.stores[0].logs.error.length).to.equal(2) + expect(pattern.stores[0].logs.error[0]).to.equal( 'Called `Path.join(that)` but `that` is not a `Path` object' ) }) @@ -1150,7 +982,7 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft() - expect(pattern.store.logs.error[0]).to.equal( + expect(pattern.stores[0].logs.error[0]).to.equal( 'Called `Path.shiftFractionAlong(fraction)` but `fraction` is not a number' ) }) @@ -1166,7 +998,7 @@ describe('Path', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft() - expect(pattern.store.logs.error[0]).to.equal( + expect(pattern.stores[0].logs.error[0]).to.equal( 'Called `Path.split(point)` but `point` is not a `Point` object' ) }) diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index 326fd4ea4b1..f64c8d7eae7 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -14,11 +14,11 @@ describe('Pattern', () => { it('Pattern constructor should add enumerable properties', () => { const Pattern = new Design() const pattern = new Pattern() - expect(typeof pattern.settings).to.equal('object') + expect(Array.isArray(pattern.settings)).to.equal(true) + expect(Array.isArray(pattern.stores)).to.equal(true) expect(typeof pattern.config).to.equal('object') - expect(typeof pattern.parts).to.equal('object') - expect(typeof pattern.store).to.equal('object') - expect(Object.keys(pattern).length).to.equal(5) + expect(typeof pattern.store).to.equal('undefined') + expect(Object.keys(pattern).length).to.equal(4) }) it('Pattern constructor should add non-enumerable properties', () => { @@ -60,7 +60,7 @@ describe('Pattern', () => { } for (const [key, value] of Object.entries(dflts)) { if (typeof value === 'object') expect(Object.keys(value).length).to.equal(0) - else expect(pattern.settings[key]).to.equal(value) + else expect(pattern.settings[0][key]).to.equal(value) } }) }) @@ -154,8 +154,8 @@ describe('Pattern', () => { }) it('Pattern.init() should set config data in the store', () => { - expect(pattern.store.get('data.name')).to.equal('test') - expect(pattern.store.get('data.version')).to.equal('1.2.3') + expect(pattern.stores[0].get('data.name')).to.equal('test') + expect(pattern.stores[0].get('data.version')).to.equal('1.2.3') }) it('Pattern.init() should resolve dependencies', () => { @@ -282,58 +282,58 @@ describe('Pattern', () => { expect(pattern.config.draftOrder[2]).to.equal('partC') expect(pattern.config.draftOrder[3]).to.equal('partR') // Points - expect(pattern.parts.partA.points.a1.x).to.equal(1) - expect(pattern.parts.partA.points.a1.y).to.equal(1) - expect(pattern.parts.partA.points.a2.x).to.equal(11) - expect(pattern.parts.partA.points.a2.y).to.equal(11) - expect(pattern.parts.partB.points.b1.x).to.equal(2) - expect(pattern.parts.partB.points.b1.y).to.equal(2) - expect(pattern.parts.partB.points.b2.x).to.equal(22) - expect(pattern.parts.partB.points.b2.y).to.equal(22) - expect(pattern.parts.partC.points.c1.x).to.equal(3) - expect(pattern.parts.partC.points.c1.y).to.equal(3) - expect(pattern.parts.partC.points.c2.x).to.equal(33) - expect(pattern.parts.partC.points.c2.y).to.equal(33) - expect(pattern.parts.partR.points.r1.x).to.equal(4) - expect(pattern.parts.partR.points.r1.y).to.equal(4) - expect(pattern.parts.partR.points.r2.x).to.equal(44) - expect(pattern.parts.partR.points.r2.y).to.equal(44) + expect(pattern.parts[0].partA.points.a1.x).to.equal(1) + expect(pattern.parts[0].partA.points.a1.y).to.equal(1) + expect(pattern.parts[0].partA.points.a2.x).to.equal(11) + expect(pattern.parts[0].partA.points.a2.y).to.equal(11) + expect(pattern.parts[0].partB.points.b1.x).to.equal(2) + expect(pattern.parts[0].partB.points.b1.y).to.equal(2) + expect(pattern.parts[0].partB.points.b2.x).to.equal(22) + expect(pattern.parts[0].partB.points.b2.y).to.equal(22) + expect(pattern.parts[0].partC.points.c1.x).to.equal(3) + expect(pattern.parts[0].partC.points.c1.y).to.equal(3) + expect(pattern.parts[0].partC.points.c2.x).to.equal(33) + expect(pattern.parts[0].partC.points.c2.y).to.equal(33) + expect(pattern.parts[0].partR.points.r1.x).to.equal(4) + expect(pattern.parts[0].partR.points.r1.y).to.equal(4) + expect(pattern.parts[0].partR.points.r2.x).to.equal(44) + expect(pattern.parts[0].partR.points.r2.y).to.equal(44) // Paths in partA - expect(pattern.parts.partA.paths.a.ops[0].to.x).to.equal(1) - expect(pattern.parts.partA.paths.a.ops[0].to.y).to.equal(1) - expect(pattern.parts.partA.paths.a.ops[1].to.x).to.equal(11) - expect(pattern.parts.partA.paths.a.ops[1].to.y).to.equal(11) + expect(pattern.parts[0].partA.paths.a.ops[0].to.x).to.equal(1) + expect(pattern.parts[0].partA.paths.a.ops[0].to.y).to.equal(1) + expect(pattern.parts[0].partA.paths.a.ops[1].to.x).to.equal(11) + expect(pattern.parts[0].partA.paths.a.ops[1].to.y).to.equal(11) // Paths in partB - expect(pattern.parts.partB.paths.a.ops[0].to.x).to.equal(1) - expect(pattern.parts.partB.paths.a.ops[0].to.y).to.equal(1) - expect(pattern.parts.partB.paths.a.ops[1].to.x).to.equal(11) - expect(pattern.parts.partB.paths.a.ops[1].to.y).to.equal(11) - expect(pattern.parts.partB.paths.b.ops[0].to.x).to.equal(2) - expect(pattern.parts.partB.paths.b.ops[0].to.y).to.equal(2) - expect(pattern.parts.partB.paths.b.ops[1].to.x).to.equal(22) - expect(pattern.parts.partB.paths.b.ops[1].to.y).to.equal(22) + expect(pattern.parts[0].partB.paths.a.ops[0].to.x).to.equal(1) + expect(pattern.parts[0].partB.paths.a.ops[0].to.y).to.equal(1) + expect(pattern.parts[0].partB.paths.a.ops[1].to.x).to.equal(11) + expect(pattern.parts[0].partB.paths.a.ops[1].to.y).to.equal(11) + expect(pattern.parts[0].partB.paths.b.ops[0].to.x).to.equal(2) + expect(pattern.parts[0].partB.paths.b.ops[0].to.y).to.equal(2) + expect(pattern.parts[0].partB.paths.b.ops[1].to.x).to.equal(22) + expect(pattern.parts[0].partB.paths.b.ops[1].to.y).to.equal(22) // Paths in partC - expect(pattern.parts.partC.paths.a.ops[0].to.x).to.equal(1) - expect(pattern.parts.partC.paths.a.ops[0].to.y).to.equal(1) - expect(pattern.parts.partC.paths.a.ops[1].to.x).to.equal(11) - expect(pattern.parts.partC.paths.a.ops[1].to.y).to.equal(11) - expect(pattern.parts.partC.paths.b.ops[0].to.x).to.equal(2) - expect(pattern.parts.partC.paths.b.ops[0].to.y).to.equal(2) - expect(pattern.parts.partC.paths.b.ops[1].to.x).to.equal(22) - expect(pattern.parts.partC.paths.b.ops[1].to.y).to.equal(22) - expect(pattern.parts.partC.paths.c.ops[0].to.x).to.equal(3) - expect(pattern.parts.partC.paths.c.ops[0].to.y).to.equal(3) - expect(pattern.parts.partC.paths.c.ops[1].to.x).to.equal(33) - expect(pattern.parts.partC.paths.c.ops[1].to.y).to.equal(33) + expect(pattern.parts[0].partC.paths.a.ops[0].to.x).to.equal(1) + expect(pattern.parts[0].partC.paths.a.ops[0].to.y).to.equal(1) + expect(pattern.parts[0].partC.paths.a.ops[1].to.x).to.equal(11) + expect(pattern.parts[0].partC.paths.a.ops[1].to.y).to.equal(11) + expect(pattern.parts[0].partC.paths.b.ops[0].to.x).to.equal(2) + expect(pattern.parts[0].partC.paths.b.ops[0].to.y).to.equal(2) + expect(pattern.parts[0].partC.paths.b.ops[1].to.x).to.equal(22) + expect(pattern.parts[0].partC.paths.b.ops[1].to.y).to.equal(22) + expect(pattern.parts[0].partC.paths.c.ops[0].to.x).to.equal(3) + expect(pattern.parts[0].partC.paths.c.ops[0].to.y).to.equal(3) + expect(pattern.parts[0].partC.paths.c.ops[1].to.x).to.equal(33) + expect(pattern.parts[0].partC.paths.c.ops[1].to.y).to.equal(33) // Paths in partR - expect(pattern.parts.partC.paths.a.ops[0].to.x).to.equal(1) - expect(pattern.parts.partC.paths.a.ops[0].to.y).to.equal(1) - expect(pattern.parts.partC.paths.a.ops[1].to.x).to.equal(11) - expect(pattern.parts.partC.paths.a.ops[1].to.y).to.equal(11) - expect(pattern.parts.partR.paths.r.ops[0].to.x).to.equal(4) - expect(pattern.parts.partR.paths.r.ops[0].to.y).to.equal(4) - expect(pattern.parts.partR.paths.r.ops[1].to.x).to.equal(44) - expect(pattern.parts.partR.paths.r.ops[1].to.y).to.equal(44) + expect(pattern.parts[0].partC.paths.a.ops[0].to.x).to.equal(1) + expect(pattern.parts[0].partC.paths.a.ops[0].to.y).to.equal(1) + expect(pattern.parts[0].partC.paths.a.ops[1].to.x).to.equal(11) + expect(pattern.parts[0].partC.paths.a.ops[1].to.y).to.equal(11) + expect(pattern.parts[0].partR.paths.r.ops[0].to.x).to.equal(4) + expect(pattern.parts[0].partR.paths.r.ops[0].to.y).to.equal(4) + expect(pattern.parts[0].partR.paths.r.ops[1].to.x).to.equal(44) + expect(pattern.parts[0].partR.paths.r.ops[1].to.y).to.equal(44) }) it('Pattern.init() should resolve nested dependencies', () => { @@ -428,54 +428,54 @@ describe('Pattern', () => { expect(pattern.config.draftOrder[2]).to.equal('partC') expect(pattern.config.draftOrder[3]).to.equal('partD') // Points - expect(pattern.parts.partA.points.a1.x).to.equal(1) - expect(pattern.parts.partA.points.a1.y).to.equal(1) - expect(pattern.parts.partA.points.a2.x).to.equal(11) - expect(pattern.parts.partA.points.a2.y).to.equal(11) - expect(pattern.parts.partB.points.b1.x).to.equal(2) - expect(pattern.parts.partB.points.b1.y).to.equal(2) - expect(pattern.parts.partB.points.b2.x).to.equal(22) - expect(pattern.parts.partB.points.b2.y).to.equal(22) - expect(pattern.parts.partC.points.c1.x).to.equal(3) - expect(pattern.parts.partC.points.c1.y).to.equal(3) - expect(pattern.parts.partC.points.c2.x).to.equal(33) - expect(pattern.parts.partC.points.c2.y).to.equal(33) - expect(pattern.parts.partD.points.d1.x).to.equal(4) - expect(pattern.parts.partD.points.d1.y).to.equal(4) - expect(pattern.parts.partD.points.d2.x).to.equal(44) - expect(pattern.parts.partD.points.d2.y).to.equal(44) + expect(pattern.parts[0].partA.points.a1.x).to.equal(1) + expect(pattern.parts[0].partA.points.a1.y).to.equal(1) + expect(pattern.parts[0].partA.points.a2.x).to.equal(11) + expect(pattern.parts[0].partA.points.a2.y).to.equal(11) + expect(pattern.parts[0].partB.points.b1.x).to.equal(2) + expect(pattern.parts[0].partB.points.b1.y).to.equal(2) + expect(pattern.parts[0].partB.points.b2.x).to.equal(22) + expect(pattern.parts[0].partB.points.b2.y).to.equal(22) + expect(pattern.parts[0].partC.points.c1.x).to.equal(3) + expect(pattern.parts[0].partC.points.c1.y).to.equal(3) + expect(pattern.parts[0].partC.points.c2.x).to.equal(33) + expect(pattern.parts[0].partC.points.c2.y).to.equal(33) + expect(pattern.parts[0].partD.points.d1.x).to.equal(4) + expect(pattern.parts[0].partD.points.d1.y).to.equal(4) + expect(pattern.parts[0].partD.points.d2.x).to.equal(44) + expect(pattern.parts[0].partD.points.d2.y).to.equal(44) // Paths in partA - expect(pattern.parts.partA.paths.a.ops[0].to.x).to.equal(1) - expect(pattern.parts.partA.paths.a.ops[0].to.y).to.equal(1) - expect(pattern.parts.partA.paths.a.ops[1].to.x).to.equal(11) - expect(pattern.parts.partA.paths.a.ops[1].to.y).to.equal(11) + expect(pattern.parts[0].partA.paths.a.ops[0].to.x).to.equal(1) + expect(pattern.parts[0].partA.paths.a.ops[0].to.y).to.equal(1) + expect(pattern.parts[0].partA.paths.a.ops[1].to.x).to.equal(11) + expect(pattern.parts[0].partA.paths.a.ops[1].to.y).to.equal(11) // Paths in partB - expect(pattern.parts.partB.paths.a.ops[0].to.x).to.equal(1) - expect(pattern.parts.partB.paths.a.ops[0].to.y).to.equal(1) - expect(pattern.parts.partB.paths.a.ops[1].to.x).to.equal(11) - expect(pattern.parts.partB.paths.a.ops[1].to.y).to.equal(11) - expect(pattern.parts.partB.paths.b.ops[0].to.x).to.equal(2) - expect(pattern.parts.partB.paths.b.ops[0].to.y).to.equal(2) - expect(pattern.parts.partB.paths.b.ops[1].to.x).to.equal(22) - expect(pattern.parts.partB.paths.b.ops[1].to.y).to.equal(22) + expect(pattern.parts[0].partB.paths.a.ops[0].to.x).to.equal(1) + expect(pattern.parts[0].partB.paths.a.ops[0].to.y).to.equal(1) + expect(pattern.parts[0].partB.paths.a.ops[1].to.x).to.equal(11) + expect(pattern.parts[0].partB.paths.a.ops[1].to.y).to.equal(11) + expect(pattern.parts[0].partB.paths.b.ops[0].to.x).to.equal(2) + expect(pattern.parts[0].partB.paths.b.ops[0].to.y).to.equal(2) + expect(pattern.parts[0].partB.paths.b.ops[1].to.x).to.equal(22) + expect(pattern.parts[0].partB.paths.b.ops[1].to.y).to.equal(22) // Paths in partC - expect(pattern.parts.partC.paths.a.ops[0].to.x).to.equal(1) - expect(pattern.parts.partC.paths.a.ops[0].to.y).to.equal(1) - expect(pattern.parts.partC.paths.a.ops[1].to.x).to.equal(11) - expect(pattern.parts.partC.paths.a.ops[1].to.y).to.equal(11) - expect(pattern.parts.partC.paths.b.ops[0].to.x).to.equal(2) - expect(pattern.parts.partC.paths.b.ops[0].to.y).to.equal(2) - expect(pattern.parts.partC.paths.b.ops[1].to.x).to.equal(22) - expect(pattern.parts.partC.paths.b.ops[1].to.y).to.equal(22) - expect(pattern.parts.partC.paths.c.ops[0].to.x).to.equal(3) - expect(pattern.parts.partC.paths.c.ops[0].to.y).to.equal(3) - expect(pattern.parts.partC.paths.c.ops[1].to.x).to.equal(33) - expect(pattern.parts.partC.paths.c.ops[1].to.y).to.equal(33) + expect(pattern.parts[0].partC.paths.a.ops[0].to.x).to.equal(1) + expect(pattern.parts[0].partC.paths.a.ops[0].to.y).to.equal(1) + expect(pattern.parts[0].partC.paths.a.ops[1].to.x).to.equal(11) + expect(pattern.parts[0].partC.paths.a.ops[1].to.y).to.equal(11) + expect(pattern.parts[0].partC.paths.b.ops[0].to.x).to.equal(2) + expect(pattern.parts[0].partC.paths.b.ops[0].to.y).to.equal(2) + expect(pattern.parts[0].partC.paths.b.ops[1].to.x).to.equal(22) + expect(pattern.parts[0].partC.paths.b.ops[1].to.y).to.equal(22) + expect(pattern.parts[0].partC.paths.c.ops[0].to.x).to.equal(3) + expect(pattern.parts[0].partC.paths.c.ops[0].to.y).to.equal(3) + expect(pattern.parts[0].partC.paths.c.ops[1].to.x).to.equal(33) + expect(pattern.parts[0].partC.paths.c.ops[1].to.y).to.equal(33) // Paths in partR - expect(pattern.parts.partD.paths.d.ops[0].to.x).to.equal(4) - expect(pattern.parts.partD.paths.d.ops[0].to.y).to.equal(4) - expect(pattern.parts.partD.paths.d.ops[1].to.x).to.equal(44) - expect(pattern.parts.partD.paths.d.ops[1].to.y).to.equal(44) + expect(pattern.parts[0].partD.paths.d.ops[0].to.x).to.equal(4) + expect(pattern.parts[0].partD.paths.d.ops[0].to.y).to.equal(4) + expect(pattern.parts[0].partD.paths.d.ops[1].to.x).to.equal(44) + expect(pattern.parts[0].partD.paths.d.ops[1].to.y).to.equal(44) }) it('Pattern.init() should load a single plugin', () => { @@ -635,30 +635,35 @@ describe('Pattern', () => { }) it('Should check whether created parts get the pattern context', () => { - const Pattern = new Design() + const part = { + name: 'test', + draft: ({ part }) => part, + } + const Pattern = new Design({ parts: [part] }) const pattern = new Pattern() - const part = pattern.__createPartWithContext('test') - expect(typeof part.context).to.equal('object') - expect(typeof part.context.parts).to.equal('object') - expect(typeof part.context.config).to.equal('object') - expect(typeof part.context.config.options).to.equal('object') - expect(typeof part.context.config.data).to.equal('object') - expect(Array.isArray(part.context.config.measurements)).to.equal(true) - expect(Array.isArray(part.context.config.optionalMeasurements)).to.equal(true) - expect(Array.isArray(part.context.config.parts)).to.equal(true) - expect(Array.isArray(part.context.config.plugins)).to.equal(true) - expect(part.context.settings).to.equal(pattern.settings) - expect(typeof part.context.store).to.equal('object') - expect(typeof part.context.store.log).to.equal('object') - expect(typeof part.context.store.log.debug).to.equal('function') - expect(typeof part.context.store.log.info).to.equal('function') - expect(typeof part.context.store.log.warning).to.equal('function') - expect(typeof part.context.store.log.error).to.equal('function') - expect(typeof part.context.store.logs).to.equal('object') - expect(Array.isArray(part.context.store.logs.debug)).to.equal(true) - expect(Array.isArray(part.context.store.logs.info)).to.equal(true) - expect(Array.isArray(part.context.store.logs.warning)).to.equal(true) - expect(Array.isArray(part.context.store.logs.error)).to.equal(true) + pattern.draft() + const context = pattern.parts[0].test.context + expect(typeof context).to.equal('object') + expect(typeof context.parts).to.equal('object') + expect(typeof context.config).to.equal('object') + expect(typeof context.config.options).to.equal('object') + expect(typeof pattern.parts[0].test.context.config.data).to.equal('object') + expect(Array.isArray(context.config.measurements)).to.equal(true) + expect(Array.isArray(context.config.optionalMeasurements)).to.equal(true) + expect(Array.isArray(context.config.parts)).to.equal(true) + expect(Array.isArray(context.config.plugins)).to.equal(true) + expect(context.settings).to.equal(pattern.settings[0]) + expect(typeof context.store).to.equal('object') + expect(typeof context.store.log).to.equal('object') + expect(typeof context.store.log.debug).to.equal('function') + expect(typeof context.store.log.info).to.equal('function') + expect(typeof context.store.log.warning).to.equal('function') + expect(typeof context.store.log.error).to.equal('function') + expect(typeof context.store.logs).to.equal('object') + expect(Array.isArray(context.store.logs.debug)).to.equal(true) + expect(Array.isArray(context.store.logs.info)).to.equal(true) + expect(Array.isArray(context.store.logs.warning)).to.equal(true) + expect(Array.isArray(context.store.logs.error)).to.equal(true) }) }) @@ -690,27 +695,27 @@ describe('Pattern', () => { pattern.init() it('Pattern settings should contain percentage options', () => { - expect(pattern.settings.options.pct).to.equal(0.3) + expect(pattern.settings[0].options.pct).to.equal(0.3) }) it('Pattern settings should contain millimeter options', () => { - expect(pattern.settings.options.mm).to.equal(12) + expect(pattern.settings[0].options.mm).to.equal(12) }) it('Pattern settings should contain degree options', () => { - expect(pattern.settings.options.deg).to.equal(2) + expect(pattern.settings[0].options.deg).to.equal(2) }) it('Pattern settings should contain list options', () => { - expect(pattern.settings.options.list).to.equal('d') + expect(pattern.settings[0].options.list).to.equal('d') }) it('Pattern settings should contain count options', () => { - expect(pattern.settings.options.count).to.equal(4) + expect(pattern.settings[0].options.count).to.equal(4) }) it('Pattern settings should contain bool options', () => { - expect(pattern.settings.options.bool).to.equal(false) + expect(pattern.settings[0].options.bool).to.equal(false) }) it('Pattern should throw an error for an unknown option', () => { diff --git a/packages/core/tests/snap.test.mjs b/packages/core/tests/snap.test.mjs index 7bfd25dc5db..c0a6f2bbe6e 100644 --- a/packages/core/tests/snap.test.mjs +++ b/packages/core/tests/snap.test.mjs @@ -10,6 +10,7 @@ describe('Snapped options', () => { it('Should snap a percentage options to equal steps', () => { const part = { name: 'test', + measurements: ['head'], options: { test: { pct: 30, min: 0, max: 100, snap: 12, toAbs }, }, @@ -17,13 +18,13 @@ describe('Snapped options', () => { const design = new Design({ parts: [part] }) const patternA = new design({ options: { test: 0.13 }, measurements }).draft() const patternB = new design({ options: { test: 0.27 }, measurements }).draft() - expect(patternA.settings.absoluteOptions.test).to.equal(60) - expect(patternB.settings.absoluteOptions.test).to.equal(108) + expect(patternA.settings[0].absoluteOptions.test).to.equal(60) + expect(patternB.settings[0].absoluteOptions.test).to.equal(108) }) - it('Should snap a percentage options to the Fibonacci sequence', () => { const part = { name: 'test', + measurements: ['head'], options: { test: { pct: 30, @@ -38,9 +39,9 @@ describe('Snapped options', () => { const patternA = new design({ options: { test: 0.13 }, measurements }).draft() const patternB = new design({ options: { test: 0.27 }, measurements }).draft() const patternC = new design({ options: { test: 0.97 }, measurements }).draft() - expect(patternA.settings.absoluteOptions.test).to.equal(55) - expect(patternB.settings.absoluteOptions.test).to.equal(89) - expect(patternC.settings.absoluteOptions.test).to.equal(388) + expect(patternA.settings[0].absoluteOptions.test).to.equal(55) + expect(patternB.settings[0].absoluteOptions.test).to.equal(89) + expect(patternC.settings[0].absoluteOptions.test).to.equal(388) }) it('Should snap a percentage options to imperial snaps', () => { @@ -64,10 +65,10 @@ describe('Snapped options', () => { const patternB = new design({ options: { test: 0.27 }, measurements, units: 'metric' }).draft() const patternC = new design({ options: { test: 0.97 }, measurements, units: 'metric' }).draft() const patternD = new design({ options: { test: 0.01 }, measurements, units: 'metric' }).draft() - expect(patternA.settings.absoluteOptions.test).to.equal(50) - expect(patternB.settings.absoluteOptions.test).to.equal(100) - expect(patternC.settings.absoluteOptions.test).to.equal(388) - expect(patternD.settings.absoluteOptions.test).to.equal(4) + expect(patternA.settings[0].absoluteOptions.test).to.equal(50) + expect(patternB.settings[0].absoluteOptions.test).to.equal(100) + expect(patternC.settings[0].absoluteOptions.test).to.equal(388) + expect(patternD.settings[0].absoluteOptions.test).to.equal(4) }) it('Should snap a percentage options to metrics snaps', () => { @@ -107,9 +108,9 @@ describe('Snapped options', () => { measurements, units: 'imperial', }).draft() - expect(patternA.settings.absoluteOptions.test).to.equal(50.8) - expect(patternB.settings.absoluteOptions.test).to.equal(101.6) - expect(patternC.settings.absoluteOptions.test).to.equal(388) - expect(patternD.settings.absoluteOptions.test).to.equal(4) + expect(patternA.settings[0].absoluteOptions.test).to.equal(50.8) + expect(patternB.settings[0].absoluteOptions.test).to.equal(101.6) + expect(patternC.settings[0].absoluteOptions.test).to.equal(388) + expect(patternD.settings[0].absoluteOptions.test).to.equal(4) }) }) diff --git a/packages/core/tests/store.test.mjs b/packages/core/tests/store.test.mjs index b4534beda13..1926c710af7 100644 --- a/packages/core/tests/store.test.mjs +++ b/packages/core/tests/store.test.mjs @@ -69,8 +69,8 @@ describe('Store', () => { const Test = new Design({ plugins: [plugin], parts: [part] }) const pattern = new Test() pattern.draft() - expect(pattern.store.get('test.message.warning')).to.equal('hello warning') - expect(pattern.store.get('test.message.info')).to.equal('hello info') + expect(pattern.stores[0].get('test.message.warning')).to.equal('hello warning') + expect(pattern.stores[0].get('test.message.info')).to.equal('hello info') }) it('Should make top-level plugin methods available via shorthand', () => { @@ -103,7 +103,7 @@ describe('Store', () => { const Test = new Design({ plugins: [plugin], parts: [part] }) const pattern = new Test() pattern.draft() - expect(pattern.store.get('test.example_part.a')).to.equal('hello A') - expect(pattern.store.get('test.example_part.b')).to.equal('hello B') + expect(pattern.stores[0].get('test.example_part.a')).to.equal('hello A') + expect(pattern.stores[0].get('test.example_part.b')).to.equal('hello B') }) }) diff --git a/packages/core/tests/utils.test.mjs b/packages/core/tests/utils.test.mjs index 0540b0a97f5..6a39614b889 100644 --- a/packages/core/tests/utils.test.mjs +++ b/packages/core/tests/utils.test.mjs @@ -32,7 +32,7 @@ const { expect } = chai describe('Utils', () => { it('Should return the correct macro name', () => { - expect(macroName('test')).to.equal('_macro_test') + expect(macroName('test')).to.equal('__macro_test') }) it('Should find the intersection of two endless line segments', () => { From f882a264083692f263d61efa58e551bdfb31744a Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Sat, 17 Sep 2022 10:30:21 +0200 Subject: [PATCH 3/4] chore(lab): Changes to handle multisets in core --- sites/shared/.eslintignore | 4 + sites/shared/.eslintrc.yml | 18 +++ .../shared/components/error/error-boundary.js | 46 ++++--- .../components/workbench/draft/error.js | 32 +++-- .../components/workbench/draft/index.js | 31 +++-- .../components/workbench/draft/part/index.js | 6 +- .../components/workbench/draft/stack.js | 4 +- .../components/workbench/draft/svg-wrapper.js | 4 +- sites/shared/components/workbench/events.js | 79 ------------ sites/shared/components/workbench/logs.js | 112 ++++++++++++++++++ .../shared/components/workbench/menu/view.js | 82 +++++++------ sites/shared/components/workbench/sample.js | 18 ++- sites/shared/components/wrappers/workbench.js | 92 +++++++------- sites/shared/config/postcss.config.js | 2 +- sites/shared/package.json | 3 + sites/shared/styles/svg-freesewing-draft.css | 17 +++ sites/shared/themes/light.js | 13 ++ yarn.lock | 62 ++++++++++ 18 files changed, 413 insertions(+), 212 deletions(-) create mode 100644 sites/shared/.eslintignore create mode 100644 sites/shared/.eslintrc.yml delete mode 100644 sites/shared/components/workbench/events.js create mode 100644 sites/shared/components/workbench/logs.js diff --git a/sites/shared/.eslintignore b/sites/shared/.eslintignore new file mode 100644 index 00000000000..b8018b39d54 --- /dev/null +++ b/sites/shared/.eslintignore @@ -0,0 +1,4 @@ +themes/* +styles/* +.eslintignore +*.css diff --git a/sites/shared/.eslintrc.yml b/sites/shared/.eslintrc.yml new file mode 100644 index 00000000000..592252726ca --- /dev/null +++ b/sites/shared/.eslintrc.yml @@ -0,0 +1,18 @@ +env: + browser: true + es2021: true +extends: + - eslint:recommended + - plugin:react/recommended +overrides: [] +parserOptions: + ecmaVersion: latest + sourceType: module +plugins: + - react +rules: + react/prop-types: off + react/react-in-jsx-scope: off +globals: + module: readonly + diff --git a/sites/shared/components/error/error-boundary.js b/sites/shared/components/error/error-boundary.js index 6ffef23816f..ac6c2367d42 100644 --- a/sites/shared/components/error/error-boundary.js +++ b/sites/shared/components/error/error-boundary.js @@ -1,55 +1,67 @@ -import React from 'react'; +import React from 'react' import ResetButtons from './reset-buttons' -import {EventGroup} from 'shared/components/workbench/events' -import DefaultErrorView from './view'; +import { LogGroup } from 'shared/components/workbench/logs' +import DefaultErrorView from './view' const ErrorView = (props) => { if (props.children) return props.children const inspectChildrenProps = { type: 'error', - events: [props.error], - units: props.gist?.units + logs: [props.error], + units: props.gist?.units, } - const inspectChildren = () - return (props.children || ( + const inspectChildren = + return ( + props.children || ( +

If you think your last action caused this error, you can:

-
)) +
+ ) + ) } class ErrorBoundary extends React.Component { constructor(props) { - super(props); - this.state = { hasError: false }; + super(props) + this.state = { hasError: false } } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. - return { hasError: true, error }; + return { hasError: true, error } } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service - console.log(error, errorInfo); + console.log(error, errorInfo) } componentDidUpdate(prevProps) { if (this.props.gist !== prevProps.gist) { - this.setState({hasError: false}) + this.setState({ hasError: false }) } } render() { if (this.state.hasError) { // You can render any custom fallback UI - return {this.errorView} + return ( + + {this.errorView} + + ) } try { - return this.props.children; - } catch(e) { - return {this.errorView}; + return this.props.children + } catch (e) { + return ( + + {this.errorView} + + ) } } } diff --git a/sites/shared/components/workbench/draft/error.js b/sites/shared/components/workbench/draft/error.js index b3c2645e6c7..0cb6bf54f64 100644 --- a/sites/shared/components/workbench/draft/error.js +++ b/sites/shared/components/workbench/draft/error.js @@ -1,18 +1,30 @@ -import DefaultErrorView from 'shared/components/error/view'; +import DefaultErrorView from 'shared/components/error/view' -const Error = ({ draft, patternProps, error, updateGist }) => { - const inspectChildren = (
    +const Error = ({ logs=[], updateGist }) => { + let errors = 0 + let warnings = 0 + for (const log of logs) { + errors += log.errors.length + warnings += log.warnings.length + } + + const inspectChildren = ( +
    • - Check the + Check the{' '} +
    • Check the partially rendered pattern below to see which areas are problematic
    • -
    ) +
+ ) - return ( -

Don't be alarmed, but we ran into some trouble while drafting this pattern.

-
) + return ( + +

No need to be alarmed, but we ran into some trouble while drafting this pattern.

+
+ ) } export default Error diff --git a/sites/shared/components/workbench/draft/index.js b/sites/shared/components/workbench/draft/index.js index b0625c095e8..dd963478399 100644 --- a/sites/shared/components/workbench/draft/index.js +++ b/sites/shared/components/workbench/draft/index.js @@ -1,16 +1,18 @@ import SvgWrapper from './svg-wrapper' import Error from './error.js' -const LabDraft = props => { - const { app, draft, gist, updateGist, unsetGist, showInfo, feedback, hasRequiredMeasurements } = props +const LabDraft = (props) => { + const { app, draft, gist, updateGist, unsetGist, showInfo, feedback, hasRequiredMeasurements } = + props if (!draft || !hasRequiredMeasurements) return null // Render as SVG if (gist?.renderer === 'svg') { let svg - try { svg = draft.render() } - catch(error) { + try { + svg = draft.render() + } catch (error) { console.log('Failed to render design', error) return } @@ -19,19 +21,26 @@ const LabDraft = props => { // Render as React let patternProps = {} - try { patternProps = draft.getRenderProps() } - catch(error) { + try { + patternProps = draft.getRenderProps() + } catch (error) { console.log('Failed to get render props for design', error) return } + const errors = [] + for (const logs of patternProps.logs) { + errors.push(...logs.error) + } + return ( <> - {(!patternProps || patternProps.logs?.error?.length > 0) - ? - : null - } - + {!patternProps || errors.length > 0 ? ( + + ) : null} + ) } diff --git a/sites/shared/components/workbench/draft/part/index.js b/sites/shared/components/workbench/draft/part/index.js index 82b35b13ed1..54b2bcb6ff3 100644 --- a/sites/shared/components/workbench/draft/part/index.js +++ b/sites/shared/components/workbench/draft/part/index.js @@ -88,7 +88,7 @@ const XrayPart = props => { export const PartInner = forwardRef((props, ref) => { const { partName, part, gist } = props - const grid = gist.paperless ? ( + const Grid = gist.paperless ? ( { ) : null return ( - {grid} + {Grid} { gist._state?.xray?.enabled && @@ -140,7 +140,7 @@ const Part = props => { const { partName, part} = props return ( - + ) diff --git a/sites/shared/components/workbench/draft/stack.js b/sites/shared/components/workbench/draft/stack.js index ec7e68fd67d..95aeea434e2 100644 --- a/sites/shared/components/workbench/draft/stack.js +++ b/sites/shared/components/workbench/draft/stack.js @@ -2,10 +2,10 @@ import Part from './part' import { getProps } from './utils' const Stack = props => { - const { stackName, stack, gist, app, updateGist, unsetGist, showInfo } = props + const { stack, gist, app, updateGist, unsetGist, showInfo } = props return ( - + {[...stack.parts].map((part) => ( { - const { patternProps, gist, app, updateGist, unsetGist, showInfo } = props + const { patternProps=false, gist, app, updateGist, unsetGist, showInfo } = props + + if (!patternProps) return null return {({ size }) => ( ( -
-    {err.stack.split(/\n/g).slice(0, 5).map((l, i) => ( 0 ? ' break-all' : '')}>{l}))}
-  
-) - -// Markdown wrapper to suppress creation of P tags -const Md = ({ children }) => props.children }}>{children} - -const Event = ({ evt, units }) => { - if (Array.isArray(evt)) { - if (evt[1]?.mm) return ${formatMm(evt[1].mm, units, 'html')}` - }}/> - else return evt.map(e => ) - } - - else if (evt.message) return - else if (typeof evt === 'string') return {evt} - - return Note a recognized event: {JSON.stringify(evt, null ,2)} -} - -export const EventGroup = ({ type='info', events=[], units='metric' }) => events.length > 0 ? ( -
-

{type}

- - - - - - - - - {events.map((evt, i) => ( - - - - - ))} - -
#Message
{i}
-
-) : null - -const order = [ - 'error', - 'warning', - 'info', - 'debug' -] -const Events = props => props?.draft?.store.logs - ? ( -
-
-
    - {order.map(type => (props.draft.store.logs[type].length > 0) - ? ( -
  • - {type} - {type === 'debug' ? '' : |} -
  • - ) : ( -
  • - {type} - {type === 'debug' ? '' : |} -
  • - ) - )} -
- {order.map(type => )} -
-
- ) : null - -export default Events diff --git a/sites/shared/components/workbench/logs.js b/sites/shared/components/workbench/logs.js new file mode 100644 index 00000000000..a88970fdf0c --- /dev/null +++ b/sites/shared/components/workbench/logs.js @@ -0,0 +1,112 @@ +import Markdown from 'react-markdown' +import { formatMm } from 'shared/utils' +import { Tab, Tabs } from '../mdx/tabs.js' + +export const Error = ({ err }) => ( +
+    {err.stack
+      .split(/\n/g)
+      .slice(0, 5)
+      .map((l, i) => (
+         0 ? ' break-all' : '')}
+        >
+          {l}
+        
+      ))}
+  
+) + +// Markdown wrapper to suppress creation of P tags +const Md = ({ children }) => ( + props.children }}>{children} +) + +const Log = ({ log, units }) => { + if (Array.isArray(log)) { + if (log[1]?.mm) + return ( + ${formatMm(log[1].mm, units, 'html')}`, + }} + /> + ) + else return log.map((l) => ) + } else if (log.message) return + else if (typeof log === 'string') return {log} + + return Unrecognized log: {JSON.stringify(log, null, 2)} +} + +export const LogGroup = ({ type = 'info', logs = [], units = 'metric' }) => + logs.length > 0 ? ( +
+

+ {type} +

+ + + + + + + + + {logs.map((log, i) => ( + + + + + ))} + +
#Message
{i} + +
+
+ ) : null + +const order = ['error', 'warning', 'info', 'debug'] + +const StoreLogs = ({ logs, units }) => ( +
+
+
    + {order.map((type) => + logs[type].length > 0 ? ( +
  • + + {type} + + {type === 'debug' ? '' : |} +
  • + ) : ( +
  • + {type} + {type === 'debug' ? '' : |} +
  • + ) + )} +
+ {order.map((type) => ( + + ))} +
+
+) + +const Logs = (props) => + props.draft.stores.length === 1 ? ( + + ) : ( + `Set ${i}`).join(',')}> + {props.draft.stores.map((store, i) => ( + + + + ))} + + ) + +export default Logs diff --git a/sites/shared/components/workbench/menu/view.js b/sites/shared/components/workbench/menu/view.js index 10732f879bf..d92b08a914b 100644 --- a/sites/shared/components/workbench/menu/view.js +++ b/sites/shared/components/workbench/menu/view.js @@ -1,90 +1,92 @@ import MenuIcon from 'shared/components/icons/menu.js' import { linkClasses, Chevron } from 'shared/components/navigation/primary.js' import { useTranslation } from 'next-i18next' -import {defaultGist} from 'shared/hooks/useGist' +import { defaultGist } from 'shared/hooks/useGist' -const View = props => { +const View = (props) => { const { t } = useTranslation(['app']) const entries = [ { name: 'measurements', title: t('measurements'), - onClick: () => props.updateGist(['_state', 'view'], 'measurements', true) + onClick: () => props.updateGist(['_state', 'view'], 'measurements', true), }, { name: 'draft', title: t('draftDesign', { design: props.design.config.data.name }), - onClick: () => props.updateGist(['_state', 'view'], 'draft', true) + onClick: () => props.updateGist(['_state', 'view'], 'draft', true), }, { name: 'test', title: t('testDesign', { design: props.design.config.data.name }), - onClick: () => props.updateGist(['_state', 'view'], 'test', true) + onClick: () => props.updateGist(['_state', 'view'], 'test', true), }, { name: 'printingLayout', - title: t('layoutThing', { thing: props.design.config.data.name }) - + ': ' + t('forPrinting'), - onClick: () => props.updateGist(['_state', 'view'], 'printingLayout', true) + title: t('layoutThing', { thing: props.design.config.data.name }) + ': ' + t('forPrinting'), + onClick: () => props.updateGist(['_state', 'view'], 'printingLayout', true), }, { name: 'cuttingLayout', - title: t('layoutThing', { thing: props.design.config.data.name }) - + ': ' + t('forCutting'), - onClick: () => props.updateGist(['_state', 'view'], 'cuttingLayout', true) + title: t('layoutThing', { thing: props.design.config.data.name }) + ': ' + t('forCutting'), + onClick: () => props.updateGist(['_state', 'view'], 'cuttingLayout', true), }, { name: 'export', title: t('exportThing', { thing: props.design.config.data.name }), - onClick: () => props.updateGist(['_state', 'view'], 'export', true) + onClick: () => props.updateGist(['_state', 'view'], 'export', true), }, { - name: 'events', - title: t('events'), - onClick: () => props.updateGist(['_state', 'view'], 'events', true) + name: 'logs', + title: t('logs'), + onClick: () => props.updateGist(['_state', 'view'], 'logs', true), }, { name: 'yaml', title: t('YAML'), - onClick: () => props.updateGist(['_state', 'view'], 'yaml', true) + onClick: () => props.updateGist(['_state', 'view'], 'yaml', true), }, { name: 'json', title: t('JSON'), - onClick: () => props.updateGist(['_state', 'view'], 'json', true) + onClick: () => props.updateGist(['_state', 'view'], 'json', true), }, { name: 'edit', title: t('editThing', { thing: 'YAML' }), - onClick: () => props.updateGist(['_state', 'view'], 'edit', true) + onClick: () => props.updateGist(['_state', 'view'], 'edit', true), }, { name: 'clear', title: t('clearThing', { thing: 'YAML' }), - onClick: () => props.setGist(defaultGist(props.design, props.gist.locale)) + onClick: () => props.setGist(defaultGist(props.design, props.gist.locale)), }, ] return ( -
- + - - - {t('view')} + `} + > + + + {t('view')}
    - {entries.map(entry => ( + {entries.map((entry) => (
  • -
  • diff --git a/sites/shared/components/workbench/sample.js b/sites/shared/components/workbench/sample.js index 29506dbe8f9..c7e6806b8cb 100644 --- a/sites/shared/components/workbench/sample.js +++ b/sites/shared/components/workbench/sample.js @@ -1,18 +1,24 @@ +import SvgWrapper from './draft/svg-wrapper' +import Error from './draft/error.js' import { svgattrPlugin } from '@freesewing/plugin-svgattr' import { useTranslation } from 'next-i18next' -const LabSample = ({ gist, draft }) => { +const LabSample = ({ gist, draft, updateGist, unsetGist, showInfo, app, feedback }) => { const { t } = useTranslation(['workbench']) let svg let title = '' + let patternProps + const errors = [] if (gist.sample) { try { draft.use(svgattrPlugin, { class: 'freesewing pattern max-h-screen' }) - draft.sample() - svg = draft.render() + draft = draft.sample() + // Render as React + patternProps = draft.getRenderProps() + for (const logs of patternProps.logs) errors.push(...logs.error) } catch(err) { console.log(err) @@ -27,6 +33,12 @@ const LabSample = ({ gist, draft }) => { return ( <>

    {title}

    + {!patternProps || errors.length > 0 ? ( + + ) : null} +
    ) diff --git a/sites/shared/components/wrappers/workbench.js b/sites/shared/components/wrappers/workbench.js index 35548de590a..c98321aa281 100644 --- a/sites/shared/components/wrappers/workbench.js +++ b/sites/shared/components/wrappers/workbench.js @@ -1,5 +1,5 @@ -import { useEffect, useState, useMemo,} from 'react' -import {useGist} from 'shared/hooks/useGist' +import { useEffect, useState, useMemo } from 'react' +import { useGist } from 'shared/hooks/useGist' import Layout from 'shared/components/layouts/default' import Menu from 'shared/components/workbench/menu/index.js' import DraftError from 'shared/components/workbench/draft/error.js' @@ -14,11 +14,11 @@ import LabSample from 'shared/components/workbench/sample.js' import ExportDraft from 'shared/components/workbench/exporting/index.js' import GistAsJson from 'shared/components/workbench/gist-as-json.js' import GistAsYaml from 'shared/components/workbench/yaml.js' -import DraftEvents from 'shared/components/workbench/events.js' +import DraftLogs from 'shared/components/workbench/logs.js' import CutLayout from 'shared/components/workbench/layout/cut' import PrintingLayout from 'shared/components/workbench/layout/print' -import ErrorBoundary from 'shared/components/error/error-boundary'; +import ErrorBoundary from 'shared/components/error/error-boundary' const views = { measurements: Measurements, @@ -27,7 +27,7 @@ const views = { printingLayout: PrintingLayout, cuttingLayout: CutLayout, export: ExportDraft, - events: DraftEvents, + logs: DraftLogs, yaml: GistAsYaml, json: GistAsJson, welcome: () =>

    TODO

    , @@ -54,15 +54,16 @@ const doPreload = async (preload, from, design, gist, setGist, setPreloaded) => * keeping the gist state, which will trickle down * to all workbench subcomponents */ -const WorkbenchWrapper = ({ app, design, preload=false, from=false, layout=false }) => { - +const WorkbenchWrapper = ({ app, design, preload = false, from = false, layout = false }) => { // State for gist - const {gist, setGist, unsetGist, updateGist, gistReady, undoGist, resetGist} = useGist(design, app); + const { gist, setGist, unsetGist, updateGist, gistReady, undoGist, resetGist } = useGist( + design, + app + ) const [messages, setMessages] = useState([]) const [popup, setPopup] = useState(false) const [preloaded, setPreloaded] = useState(false) - // We'll use this in more than one location const hasRequiredMeasurements = hasRequiredMeasurementsMethod(design, gist) @@ -70,34 +71,31 @@ const WorkbenchWrapper = ({ app, design, preload=false, from=false, layout=false // force view to measurements useEffect(() => { if (!gistReady) return - if (gist._state?.view !== 'measurements' - && !hasRequiredMeasurements - ) updateGist(['_state', 'view'], 'measurements') + if (gist._state?.view !== 'measurements' && !hasRequiredMeasurements) + updateGist(['_state', 'view'], 'measurements') }, [gistReady, gist._state?.view, hasRequiredMeasurements]) // If we need to preload the gist, do so useEffect(() => { - if ( - preload && - preload !== preloaded && - from && - preloaders[from] - ) { - doPreload(preload, from, design, gist, setGist, setPreloaded) + if (preload && preload !== preloaded && from && preloaders[from]) { + doPreload(preload, from, design, gist, setGist, setPreloaded) } }, [preload, preloaded, from, design]) - // Helper methods to manage the gist state - const updateWBGist = useMemo(() => (path, value, closeNav=false, addToHistory=true) => { - updateGist(path, value, addToHistory) - // Force close of menu on mobile if it is open - if (closeNav && app.primaryMenu) app.setPrimaryMenu(false) - }, [app]) + const updateWBGist = useMemo( + () => + (path, value, closeNav = false, addToHistory = true) => { + updateGist(path, value, addToHistory) + // Force close of menu on mobile if it is open + if (closeNav && app.primaryMenu) app.setPrimaryMenu(false) + }, + [app] + ) // Helper methods to handle messages const feedback = { - add: msg => { + add: (msg) => { const newMsgs = [...messages] if (Array.isArray(msg)) newMsgs.push(...msg) else newMsgs.push(msg) @@ -108,16 +106,18 @@ const WorkbenchWrapper = ({ app, design, preload=false, from=false, layout=false } // don't do anything until the gist is ready - if (!gistReady) {return null} + if (!gistReady) { + return null + } // Generate the draft here so we can pass it down to both the view and the options menu let draft = false - if (['draft', 'events', 'test', 'printingLayout'].indexOf(gist._state?.view) !== -1) { + if (['draft', 'logs', 'test', 'printingLayout'].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 // hand it separately to the design - draft = new design({...gist, layout}) + draft = new design({ ...gist, layout }) draft.init() // add theme to svg renderer @@ -125,9 +125,8 @@ const WorkbenchWrapper = ({ app, design, preload=false, from=false, layout=false // draft it for draft and event views. Other views may add plugins, etc and we don't want to draft twice try { - if (['draft', 'events'].indexOf(gist._state.view) > -1) draft.draft() - } - catch(error) { + if (['draft', 'logs'].indexOf(gist._state.view) > -1) draft.draft() + } catch (error) { console.log('Failed to draft design', error) return } @@ -152,33 +151,30 @@ const WorkbenchWrapper = ({ app, design, preload=false, from=false, layout=false app: app, noSearch: true, workbench: true, - AltMenu: , + AltMenu: , showInfo: setPopup, } const errorProps = { undoGist, resetGist, - gist + gist, } // Layout to use - const LayoutComponent = layout - ? layout - : Layout + const LayoutComponent = layout ? layout : Layout - const Component = views[gist._state?.view] - ? views[gist._state.view] - : views.welcome + const Component = views[gist._state?.view] ? views[gist._state.view] : views.welcome - return - {messages} - - - {popup && setPopup(false)}>{popup}} - - + return ( + + {messages} + + + {popup && setPopup(false)}>{popup}} + + + ) } export default WorkbenchWrapper - diff --git a/sites/shared/config/postcss.config.js b/sites/shared/config/postcss.config.js index 8aeb8379b4a..5e92fafc535 100644 --- a/sites/shared/config/postcss.config.js +++ b/sites/shared/config/postcss.config.js @@ -1,4 +1,4 @@ // Can't seem to make this work as ESM module.exports = { - plugins: ['tailwindcss/nesting', 'tailwindcss', 'autoprefixer'], + plugins: ['tailwindcss/nesting', 'tailwindcss', 'autoprefixer', 'postcss-for'], } diff --git a/sites/shared/package.json b/sites/shared/package.json index c91f3314416..b3dbcc730aa 100644 --- a/sites/shared/package.json +++ b/sites/shared/package.json @@ -28,6 +28,7 @@ "lodash.unset": "^4.5.2", "mdast-util-toc": "^6.1.0", "pdfkit": "^0.13.0", + "postcss-for": "^2.1.1", "react-markdown": "^8.0.0", "react-sizeme": "^3.0.2", "react-timeago": "^7.1.0", @@ -44,6 +45,8 @@ }, "devDependencies": { "autoprefixer": "^10.4.0", + "eslint": "^8.23.1", + "eslint-plugin-react": "^7.31.8", "lodash.set": "^4.3.2", "postcss": "^8.4.4", "tailwindcss": "^3.0.1", diff --git a/sites/shared/styles/svg-freesewing-draft.css b/sites/shared/styles/svg-freesewing-draft.css index 88b2ef7988f..9cf2db6d879 100644 --- a/sites/shared/styles/svg-freesewing-draft.css +++ b/sites/shared/styles/svg-freesewing-draft.css @@ -161,6 +161,20 @@ svg.freesewing.pattern { } } +/* Styling for v3 sampling */ +@for $i from 1 to 10 { + + svg.freesewing.pattern g.sample-$i path.fabric, + svg.freesewing.pattern g.sample-$i path.lining, + svg.freesewing.pattern g.sample-$i path.interfacing { + stroke: var(--pattern-sample-$i); + fill: var(--pattern-sample-$i); + fill-opacity: 0.01; + } + +} + + /* Styling the shadow DOM is hard to do * This is for styling FreeSewing snippets * TODO: Update snippets to use inline styles with CSS vars @@ -257,3 +271,6 @@ figure.develop.example div.develop { } } + + + diff --git a/sites/shared/themes/light.js b/sites/shared/themes/light.js index 5904970a87f..8e05a2a6f04 100644 --- a/sites/shared/themes/light.js +++ b/sites/shared/themes/light.js @@ -257,4 +257,17 @@ module.exports = { '--pattern-stroke-6xl': "16px", // Pattern 7xl stroke width '--pattern-stroke-7xl': "20px", + + // Pattern sampling styles + '--pattern-sample-1': colors.red["500"], + '--pattern-sample-2': colors.orange["500"], + '--pattern-sample-3': colors.yellow["500"], + '--pattern-sample-4': colors.lime["500"], + '--pattern-sample-5': colors.emerald["500"], + '--pattern-sample-6': colors.cyan["500"], + '--pattern-sample-7': colors.blue["500"], + '--pattern-sample-8': colors.violet["500"], + '--pattern-sample-9': colors.fuchsia["500"], + '--pattern-sample-10': colors.rose["500"], + } diff --git a/yarn.lock b/yarn.lock index 0d07dc1582e..89a50ceeded 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7934,6 +7934,26 @@ eslint-plugin-react@^7.28.0, eslint-plugin-react@^7.29.4: semver "^6.3.0" string.prototype.matchall "^4.0.7" +eslint-plugin-react@^7.31.8: + version "7.31.8" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz#3a4f80c10be1bcbc8197be9e8b641b2a3ef219bf" + integrity sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw== + dependencies: + array-includes "^3.1.5" + array.prototype.flatmap "^1.3.0" + doctrine "^2.1.0" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.5" + object.fromentries "^2.0.5" + object.hasown "^1.1.1" + object.values "^1.1.5" + prop-types "^15.8.1" + resolve "^2.0.0-next.3" + semver "^6.3.0" + string.prototype.matchall "^4.0.7" + eslint-plugin-yaml@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/eslint-plugin-yaml/-/eslint-plugin-yaml-0.5.0.tgz#8c79d9d6389b67cbcf58ef6f970c4c086665a63a" @@ -9555,6 +9575,11 @@ has-bigints@^1.0.1, has-bigints@^1.0.2: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -11124,6 +11149,11 @@ jest-validate@^27.3.1, jest-validate@^27.4.2: leven "^3.1.0" pretty-format "^27.5.1" +js-base64@^2.1.9: + version "2.6.4" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" + integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== + js-sdsl@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6" @@ -15346,6 +15376,14 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== +postcss-for@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-for/-/postcss-for-2.1.1.tgz#841378c0ef909d50e1980d5aa71e6a340e728fcd" + integrity sha512-X0R84FCyr5cqzW4+/g4Dvz2OUe1iwC3G/atIrwEpiRstZlBBpknV+ETlIneSTnw/iXgUnEoTRaO2qXY62YWLhQ== + dependencies: + postcss "^5.0.0" + postcss-simple-vars "^2.0.0" + postcss-import@^14.1.0: version "14.1.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" @@ -15385,6 +15423,13 @@ postcss-selector-parser@6.0.10, postcss-selector-parser@^6.0.10, postcss-selecto cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-simple-vars@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-2.0.0.tgz#d0a1091b0da22b79507028f7b22b976c0a60b8d5" + integrity sha512-HllLaKKCBOdKudyzqrw/ve5rWouM9cDL+WHaSF9q4CkBEPjdTdiKNw1xF2dAz5rUKrxVmnUmOYxamwy37dnq2Q== + dependencies: + postcss "^5.0.21" + postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" @@ -15408,6 +15453,16 @@ postcss@8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^5.0.0, postcss@^5.0.21: + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" + integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + postcss@^8.4.12, postcss@^8.4.14, postcss@^8.4.4, postcss@^8.4.5: version "8.4.16" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" @@ -18188,6 +18243,13 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + integrity sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A== + dependencies: + has-flag "^1.0.0" + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" From 200cebf5822249c48727aee0cc2fbc9352da0932 Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Sun, 18 Sep 2022 15:11:10 +0200 Subject: [PATCH 4/4] chore(core): Refactor v3 code --- .gitignore | 1 + config/scripts.yaml | 1 + packages/core/.eslintignore | 1 + packages/core/jsdoc.json | 17 + packages/core/package.json | 1 + packages/core/src/attributes.mjs | 211 ++- packages/core/src/config.mjs | 18 +- packages/core/src/design.mjs | 17 +- packages/core/src/hooks.mjs | 6 + packages/core/src/index.mjs | 79 +- packages/core/src/option.mjs | 7 - packages/core/src/part.mjs | 442 +++--- packages/core/src/path.mjs | 1653 ++++++++++++-------- packages/core/src/pattern.mjs | 1466 +++++++++-------- packages/core/src/point.mjs | 558 ++++--- packages/core/src/snippet.mjs | 96 +- packages/core/src/stack.mjs | 46 +- packages/core/src/store.mjs | 113 +- packages/core/src/svg.mjs | 557 ++++--- packages/core/src/utils.mjs | 897 +++++++---- packages/core/tests/multi.test.mjs | 3 - packages/core/tests/part.test.mjs | 14 +- packages/core/tests/path.test.mjs | 92 +- packages/core/tests/pattern-draft.test.mjs | 12 +- packages/core/tests/point.test.mjs | 126 +- packages/core/tests/utils.test.mjs | 35 - yarn.lock | 125 +- 27 files changed, 3961 insertions(+), 2633 deletions(-) create mode 100644 packages/core/.eslintignore create mode 100644 packages/core/jsdoc.json delete mode 100644 packages/core/src/option.mjs diff --git a/.gitignore b/.gitignore index 04e44de64de..516cf7c3eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,7 @@ node_modules dist build export +packages/core/out # Prebuild files prebuild/*.json diff --git a/config/scripts.yaml b/config/scripts.yaml index 0ac37b74cb8..520f8bcede2 100644 --- a/config/scripts.yaml +++ b/config/scripts.yaml @@ -23,6 +23,7 @@ core: testci: 'mocha tests/*.test.mjs' prettier: "npx prettier --write 'src/*.mjs' 'tests/*.mjs'" lint: "npx eslint 'src/*.mjs' 'tests/*.mjs'" + jsdoc: "jsdoc -c jsdoc.json -r src" i18n: prebuild: 'node scripts/prebuilder.mjs' models: diff --git a/packages/core/.eslintignore b/packages/core/.eslintignore new file mode 100644 index 00000000000..d00eaf8faf5 --- /dev/null +++ b/packages/core/.eslintignore @@ -0,0 +1 @@ +jsdoc.json diff --git a/packages/core/jsdoc.json b/packages/core/jsdoc.json new file mode 100644 index 00000000000..f5994b0754d --- /dev/null +++ b/packages/core/jsdoc.json @@ -0,0 +1,17 @@ +{ + "plugins": [], + "recurseDepth": 2, + "source": { + "includePattern": ".+\\.mjs?$", + "excludePattern": "(^|\\/|\\\\)_" + }, + "sourceType": "module", + "tags": { + "allowUnknownTags": true, + "dictionaries": ["jsdoc","closure"] + }, + "templates": { + "cleverLinks": false, + "monospaceLinks": false + } +} diff --git a/packages/core/package.json b/packages/core/package.json index d0840959483..3e926a2dd4d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -43,6 +43,7 @@ "report": "c8 report", "testci": "mocha tests/*.test.mjs", "prettier": "npx prettier --write 'src/*.mjs' 'tests/*.mjs'", + "jsdoc": "jsdoc -c jsdoc.json -r src", "cibuild_step0": "node build.mjs" }, "peerDependencies": {}, diff --git a/packages/core/src/attributes.mjs b/packages/core/src/attributes.mjs index 4108a487da3..f04a1211b67 100644 --- a/packages/core/src/attributes.mjs +++ b/packages/core/src/attributes.mjs @@ -1,8 +1,30 @@ +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// + +/** + * Constructor for Attributes + * + * @constructor + * @return {Attributes} this - The Attributes instance + */ export function Attributes() { this.list = {} + + return this } -/** Adds an attribute */ +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// + +/** + * Add an attribute + * + * @param {string} name - Name of the attribute to add + * @param {string} value - Value of the attribute to add + * @return {Attributes} this - The Attributes instance + */ Attributes.prototype.add = function (name, value) { if (typeof this.list[name] === 'undefined') { this.list[name] = [] @@ -12,75 +34,12 @@ Attributes.prototype.add = function (name, value) { return this } -/** Sets an attribute, overwriting existing value */ -Attributes.prototype.set = function (name, value) { - this.list[name] = [value] - - return this -} - -/** Sets an attribute, but only if it's not currently set */ -Attributes.prototype.setIfUnset = function (name, value) { - if (typeof this.list[name] === 'undefined') this.list[name] = [value] - - return this -} - -/** Removes an attribute */ -Attributes.prototype.remove = function (name) { - delete this.list[name] - - return this -} - -/** Retrieves an attribute */ -Attributes.prototype.get = function (name) { - if (typeof this.list[name] === 'undefined') return false - else return this.list[name].join(' ') -} - -/** Retrieves an attribute as array*/ -Attributes.prototype.getAsArray = function (name) { - if (typeof this.list[name] === 'undefined') return false - else return this.list[name] -} - -/** Returns SVG code for attributes */ -Attributes.prototype.render = function () { - let svg = '' - for (let key in this.list) { - svg += ` ${key}="${this.list[key].join(' ')}"` - } - - return svg -} - -/** Returns CSS code for attributes */ -Attributes.prototype.renderAsCss = function () { - let css = '' - for (let key in this.list) { - css += ` ${key}:${this.list[key].join(' ')};` - } - - return css -} - -/** Returns SVG code for attributes with a fiven prefix - * typically used for data-text*/ -Attributes.prototype.renderIfPrefixIs = function (prefix = '') { - let svg = '' - let prefixLen = prefix.length - for (let key in this.list) { - if (key.substr(0, prefixLen) === prefix) { - svg += ` ${key.substr(prefixLen)}="${this.list[key].join(' ')}"` - } - } - - return svg -} - -/** Returns a props object for attributes with a fiven prefix - * typically used for data-text*/ +/** + * Return a props object for attributes with a fiven prefix (typically used for data-text) + * + * @param {string} prefix - The prefix to filter attributes on + * @return {object} props - The attributes as props + */ Attributes.prototype.asPropsIfPrefixIs = function (prefix = '') { let props = {} let prefixLen = prefix.length @@ -95,10 +54,120 @@ Attributes.prototype.asPropsIfPrefixIs = function (prefix = '') { return props } -/** Returns a deep copy of this */ +/** + * Return a deep copy of this + * + * @return {object} this - The Attributes instance + */ Attributes.prototype.clone = function () { let clone = new Attributes() clone.list = JSON.parse(JSON.stringify(this.list)) return clone } + +/** + * Retrieve an attribute + * + * @param {string} name - Name of the attribute to get + * @return value - The value under name + */ +Attributes.prototype.get = function (name) { + if (typeof this.list[name] === 'undefined') return false + else return this.list[name].join(' ') +} + +/** + * Retrieve an attribute as array + * + * @param {string} name - Name of the attribute to set + * @return {object} this - The Attributes instance + */ +Attributes.prototype.getAsArray = function (name) { + if (typeof this.list[name] === 'undefined') return false + else return this.list[name] +} + +/** + * Remove an attribute + * + * @param {string} name - Name of the attribute to set + * @return {object} this - The Attributes instance + */ +Attributes.prototype.remove = function (name) { + delete this.list[name] + + return this +} + +/** + * Return SVG code for attributes + * + * @return {string} svg - The SVG code + */ +Attributes.prototype.render = function () { + let svg = '' + for (let key in this.list) { + svg += ` ${key}="${this.list[key].join(' ')}"` + } + + return svg +} + +/** + * Return CSS code for attributes + * + * @return {string} css - The CSS code + */ +Attributes.prototype.renderAsCss = function () { + let css = '' + for (let key in this.list) { + css += ` ${key}:${this.list[key].join(' ')};` + } + + return css +} + +/** + * Return SVG code for attributes with a fiven prefix (typically used for data-text) + * + * @param {string} prefix - The prefix to filter attributes on + * @return {string} svg - The SVG code + */ +Attributes.prototype.renderIfPrefixIs = function (prefix = '') { + let svg = '' + let prefixLen = prefix.length + for (let key in this.list) { + if (key.substr(0, prefixLen) === prefix) { + svg += ` ${key.substr(prefixLen)}="${this.list[key].join(' ')}"` + } + } + + return svg +} + +/** + * Set an attribute, overwriting existing value + * + * @param {string} name - Name of the attribute to set + * @param {string} value - Value of the attribute to set + * @return {Attributes} this - The Attributes instance + */ +Attributes.prototype.set = function (name, value) { + this.list[name] = [value] + + return this +} + +/** + * Sets an attribute, but only if it's not currently set + * + * @param {string} name - Name of the attribute to set + * @param {string} value - Value of the attribute to set + * @return {Attributes} this - The Attributes instance + */ +Attributes.prototype.setIfUnset = function (name, value) { + if (typeof this.list[name] === 'undefined') this.list[name] = [value] + + return this +} diff --git a/packages/core/src/config.mjs b/packages/core/src/config.mjs index 9b6115f9624..9b775eb8e6c 100644 --- a/packages/core/src/config.mjs +++ b/packages/core/src/config.mjs @@ -1,4 +1,11 @@ -export const loadDesignDefaults = () => ({ +/** + * Return an object holding the defaults for a design configuration + * + * @function + * @private + * @return {object} defaults - The default design configuration + */ +export const __loadDesignDefaults = () => ({ measurements: [], optionalMeasurements: [], options: {}, @@ -8,7 +15,14 @@ export const loadDesignDefaults = () => ({ plugins: [], }) -export const loadPatternDefaults = () => ({ +/** + * Return an object holding the defaults for pattern settings + * + * @function + * @private + * @return {object} defaults - The default pattern settings + */ +export const __loadPatternDefaults = () => ({ complete: true, idPrefix: 'fs-', stackPrefix: '', diff --git a/packages/core/src/design.mjs b/packages/core/src/design.mjs index 7a06031e18b..68e629224ef 100644 --- a/packages/core/src/design.mjs +++ b/packages/core/src/design.mjs @@ -1,13 +1,20 @@ import { Pattern } from './pattern.mjs' -import { loadDesignDefaults } from './config.mjs' +import { __loadDesignDefaults } from './config.mjs' -/* - * The Design constructor. Returns a Pattern constructor - * So it's sort of a super-constructor +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// + +/** + * Return a Pattern constructor (it's a super-constructor) + * + * @constructor + * @param {object} config - The design configuration + * @return {function} pattern - The pattern constructor */ export function Design(config) { // Initialize config with defaults - config = { ...loadDesignDefaults(), ...config } + config = { ...__loadDesignDefaults(), ...config } // Create the pattern constructor const pattern = function (...sets) { diff --git a/packages/core/src/hooks.mjs b/packages/core/src/hooks.mjs index 599bea25091..1a078d4aecb 100644 --- a/packages/core/src/hooks.mjs +++ b/packages/core/src/hooks.mjs @@ -1,3 +1,9 @@ +/** + * Returns an object holding the defaults hooks structure + * + * @constructor + * @return {object} hooks - The default hooks holding structure + */ export function Hooks() { return { preDraft: [], diff --git a/packages/core/src/index.mjs b/packages/core/src/index.mjs index bc7109f0e08..0d4316b4651 100644 --- a/packages/core/src/index.mjs +++ b/packages/core/src/index.mjs @@ -1,3 +1,4 @@ +import { Bezier } from 'bezier-js' import { Attributes } from './attributes.mjs' import { Design } from './design.mjs' import { Pattern } from './pattern.mjs' @@ -7,34 +8,30 @@ import { Path } from './path.mjs' import { Snippet } from './snippet.mjs' import { Store } from './store.mjs' import { - isCoord, - capitalize, - beamsIntersect, - linesIntersect, - pointOnBeam, - pointOnLine, - pointOnCurve, - splitCurve, + beamIntersectsCircle, beamIntersectsX, beamIntersectsY, - units, - lineIntersectsCurve, + beamsIntersect, + capitalize, + circlesIntersect, + curveEdge, curveIntersectsX, curveIntersectsY, curvesIntersect, - circlesIntersect, - beamIntersectsCircle, - lineIntersectsCircle, - curveEdge, - stretchToScale, - round, - sampleStyle, deg2rad, - rad2deg, - pctBasedOn, - Bezier, generateStackTransform, - macroName, + lineIntersectsCircle, + lineIntersectsCurve, + linesIntersect, + pctBasedOn, + pointOnBeam, + pointOnCurve, + pointOnLine, + rad2deg, + round, + splitCurve, + stretchToScale, + units, } from './utils.mjs' import { version } from '../data.mjs' @@ -48,33 +45,31 @@ export { Part, Snippet, Store, + version, Bezier, - capitalize, - beamsIntersect, - linesIntersect, - pointOnBeam, - pointOnLine, - pointOnCurve, - splitCurve, + // Utils + beamIntersectsCircle, beamIntersectsX, beamIntersectsY, - units, - lineIntersectsCurve, + beamsIntersect, + capitalize, + circlesIntersect, + curveEdge, curveIntersectsX, curveIntersectsY, curvesIntersect, - circlesIntersect, - beamIntersectsCircle, - lineIntersectsCircle, - curveEdge, - stretchToScale, - round, - sampleStyle, deg2rad, - rad2deg, - pctBasedOn, generateStackTransform, - macroName, - isCoord, - version, + lineIntersectsCircle, + lineIntersectsCurve, + linesIntersect, + pctBasedOn, + pointOnBeam, + pointOnCurve, + pointOnLine, + rad2deg, + round, + splitCurve, + stretchToScale, + units, } diff --git a/packages/core/src/option.mjs b/packages/core/src/option.mjs deleted file mode 100644 index e579e7b5991..00000000000 --- a/packages/core/src/option.mjs +++ /dev/null @@ -1,7 +0,0 @@ -export function Option(config) { - this.id = config.id - this.config = config - this.val = config.val - - return this -} diff --git a/packages/core/src/part.mjs b/packages/core/src/part.mjs index d0bc8b17e27..703d5c8cf7f 100644 --- a/packages/core/src/part.mjs +++ b/packages/core/src/part.mjs @@ -1,23 +1,33 @@ import { Attributes } from './attributes.mjs' import * as utils from './utils.mjs' -import { Point } from './point.mjs' -import { Path } from './path.mjs' -import { Snippet } from './snippet.mjs' +import { Point, pointsProxy } from './point.mjs' +import { Path, pathsProxy } from './path.mjs' +import { Snippet, snippetsProxy } from './snippet.mjs' import { Hooks } from './hooks.mjs' +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// + +/** + * Constructor for a Part + * + * @constructor + * @return {Part} this - The Part instance + */ export function Part() { // Non-enumerable properties - utils.addNonEnumProp(this, 'freeId', 0) - utils.addNonEnumProp(this, 'topLeft', false) - utils.addNonEnumProp(this, 'bottomRight', false) - utils.addNonEnumProp(this, 'width', false) - utils.addNonEnumProp(this, 'height', false) - utils.addNonEnumProp(this, 'utils', utils) - utils.addNonEnumProp(this, 'layout', { move: { x: 0, y: 0 } }) - utils.addNonEnumProp(this, 'Point', Point) - utils.addNonEnumProp(this, 'Path', Path) - utils.addNonEnumProp(this, 'Snippet', Snippet) - utils.addNonEnumProp(this, 'hooks', new Hooks()) + utils.__addNonEnumProp(this, 'freeId', 0) + utils.__addNonEnumProp(this, 'topLeft', false) + utils.__addNonEnumProp(this, 'bottomRight', false) + utils.__addNonEnumProp(this, 'width', false) + utils.__addNonEnumProp(this, 'height', false) + utils.__addNonEnumProp(this, 'utils', utils) + utils.__addNonEnumProp(this, 'layout', { move: { x: 0, y: 0 } }) + utils.__addNonEnumProp(this, 'Point', Point) + utils.__addNonEnumProp(this, 'Path', Path) + utils.__addNonEnumProp(this, 'Snippet', Snippet) + utils.__addNonEnumProp(this, 'hooks', new Hooks()) // Enumerable properties this.render = true // FIXME: Replace render with hide @@ -31,56 +41,163 @@ export function Part() { return this } -Part.prototype.macroClosure = function () { - let self = this - let method = function (key, args) { - let macro = utils.macroName(key) - if (typeof self[macro] === 'function') self[macro](args) - } +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// - return method +/** + * Adds an attribute in a chainable way + * + * @param {string} name - Name of the attribute to add + * @param {string} value - Value of the attribute to add + * @param {bool} overwrite - Whether to overwrite an existing attrubute or not + * @return {Part} this - The part instance + */ +Part.prototype.attr = function (name, value, overwrite = false) { + if (overwrite) this.attributes.set(name, value) + else this.attributes.add(name, value) + + return this } -Part.prototype.runHooks = function (hookName, data = false) { - if (data === false) data = this - let hooks = this.hooks[hookName] - if (hooks && hooks.length > 0) { - for (let hook of hooks) { - hook.method(data, hook.data) - } - } -} - -/** Returns an unused ID */ +/** + * Returns on unused ID (unused in this part) + * + * @param {string} prefix - An optional prefix to apply to the ID + * @return {string} id - The id + */ Part.prototype.getId = function (prefix = '') { this.freeId += 1 return prefix + this.freeId } -/** Returns a value formatted for units provided in settings */ -Part.prototype.unitsClosure = function () { - const self = this - const method = function (value) { - if (typeof value !== 'number') - self.context.store.log.warning( - `Calling \`units(value)\` but \`value\` is not a number (\`${typeof value}\`)` - ) - return utils.units(value, self.context.settings.units) +/** Returns an object with shorthand access for pattern design */ +/** + * Returns an object that will be passed to draft method to be destructured + * + * @return {object} short - The so-called shorthand object with what you might need in your draft method + */ +Part.prototype.shorthand = function () { + const complete = this.context.settings?.complete ? true : false + const paperless = this.context.settings?.paperless === true ? true : false + const sa = this.context.settings?.complete ? this.context.settings?.sa || 0 : 0 + const shorthand = { + part: this, + sa, + scale: this.context.settings?.scale, + store: this.context.store, + macro: this.__macroClosure(), + units: this.__unitsClosure(), + utils: utils, + complete, + paperless, + events: this.context.events, + log: this.context.store.log, + addCut: this.addCut, + removeCut: this.removeCut, + } + // Add top-level store methods and add a part name parameter + const partName = this.name + for (const [key, method] of Object.entries(this.context.store)) { + if (typeof method === 'function') + shorthand[key] = function (...args) { + return method(partName, ...args) + } } - return method + // We'll need this + let self = this + + // Wrap the Point constructor so objects can log + shorthand.Point = function (x, y) { + Point.apply(this, [x, y]) + Object.defineProperty(this, 'log', { value: self.context.store.log }) + } + shorthand.Point.prototype = Object.create(Point.prototype) + // Wrap the Path constructor so objects can log + shorthand.Path = function () { + Path.apply(this, [true]) + Object.defineProperty(this, 'log', { value: self.context.store.log }) + } + shorthand.Path.prototype = Object.create(Path.prototype) + // Wrap the Snippet constructor so objects can log + shorthand.Snippet = function (def, anchor) { + Snippet.apply(this, [def, anchor, true]) + Snippet.apply(this, arguments) + Object.defineProperty(this, 'log', { value: self.context.store.log }) + } + shorthand.Snippet.prototype = Object.create(Snippet.prototype) + + // Proxy points, paths, snippets, measurements, options, and absoluteOptions + shorthand.points = new Proxy(this.points || {}, pointsProxy(self.points, self.context.store.log)) + shorthand.paths = new Proxy(this.paths || {}, pathsProxy(self.paths, self.context.store.log)) + shorthand.snippets = new Proxy( + this.snippets || {}, + snippetsProxy(self.snippets, self.context.store.log) + ) + shorthand.measurements = new Proxy(this.context.settings.measurements || {}, { + get: function (measurements, name) { + if (typeof measurements[name] === 'undefined') + self.context.store.log.warning( + `Tried to access \`measurements.${name}\` but it is \`undefined\`` + ) + return Reflect.get(...arguments) + }, + set: (measurements, name, value) => (self.context.settings.measurements[name] = value), + }) + shorthand.options = new Proxy(this.context.settings.options || {}, { + get: function (options, name) { + if (typeof options[name] === 'undefined') + self.context.store.log.warning( + `Tried to access \`options.${name}\` but it is \`undefined\`` + ) + return Reflect.get(...arguments) + }, + set: (options, name, value) => (self.context.settings.options[name] = value), + }) + shorthand.absoluteOptions = new Proxy(this.context.settings.absoluteOptions || {}, { + get: function (absoluteOptions, name) { + if (typeof absoluteOptions[name] === 'undefined') + self.context.store.log.warning( + `Tried to access \`absoluteOptions.${name}\` but it is \`undefined\`` + ) + return Reflect.get(...arguments) + }, + set: (absoluteOptions, name, value) => (self.context.settings.absoluteOptions[name] = value), + }) + + return shorthand } -/** Calculates the part's bounding box and sets it */ -Part.prototype.boundary = function () { +/** + * Returns a value formatted for units set in settings + * + * @param {float} input - The value to format + * @return {string} result - The input formatted for the units set in settings + */ +Part.prototype.units = function (input) { + return utils.units(input, this.context.settings.units) +} + +////////////////////////////////////////////// +// PRIVATE METHODS // +////////////////////////////////////////////// + +/** + * Calculates the part's bounding box and mutates the part to set it + * + * @private + * @return {Part} this - The part instance + */ +Part.prototype.__boundary = function () { if (this.topLeft) return this // Cached let topLeft = new Point(Infinity, Infinity) let bottomRight = new Point(-Infinity, -Infinity) for (let key in this.paths) { try { - let path = this.paths[key].boundary() + let path = this.paths[key].__boundary() if (path.render) { if (path.topLeft.x < topLeft.x) topLeft.x = path.topLeft.x if (path.topLeft.y < topLeft.y) topLeft.y = path.topLeft.y @@ -120,16 +237,14 @@ Part.prototype.boundary = function () { return this } -/** Adds an attribute. This is here to make this call chainable in assignment */ -Part.prototype.attr = function (name, value, overwrite = false) { - if (overwrite) this.attributes.set(name, value) - else this.attributes.add(name, value) - - return this -} - -/** Copies point/path/snippet data from part orig into this */ -Part.prototype.inject = function (orig) { +/** + * Copies point/path/snippet data from part orig into this + * + * @private + * @param {object} orig - The original part to inject into this + * @return {Part} this - The part instance + */ +Part.prototype.__inject = function (orig) { const findBasePoint = (p) => { for (let i in orig.points) { if (orig.points[i] === p) return i @@ -161,196 +276,37 @@ Part.prototype.inject = function (orig) { return this } -Part.prototype.units = function (input) { - return utils.units(input, this.context.settings.units) -} - -/** Returns an object with shorthand access for pattern design */ -Part.prototype.shorthand = function () { - const complete = this.context.settings?.complete ? true : false - const paperless = this.context.settings?.paperless === true ? true : false - const sa = this.context.settings?.complete ? this.context.settings?.sa || 0 : 0 - const shorthand = { - part: this, - sa, - scale: this.context.settings?.scale, - store: this.context.store, - macro: this.macroClosure(), - units: this.unitsClosure(), - utils: utils, - complete, - paperless, - events: this.context.events, - log: this.context.store.log, - addCut: this.addCut, - removeCut: this.removeCut, - } - // Add top-level store methods and add a part name parameter - const partName = this.name - for (const [key, method] of Object.entries(this.context.store)) { - if (typeof method === 'function') - shorthand[key] = function (...args) { - return method(partName, ...args) - } - } - - // We'll need this +/** + * Returns a closure holding the macro method + * + * @private + * @return {function} method - The closured macro method + */ +Part.prototype.__macroClosure = function () { let self = this + let method = function (key, args) { + let macro = utils.__macroName(key) + if (typeof self[macro] === 'function') self[macro](args) + } - // Wrap the Point constructor so objects can log - shorthand.Point = function (x, y) { - Point.apply(this, [x, y, true]) - Object.defineProperty(this, 'log', { value: self.context.store.log }) - } - shorthand.Point.prototype = Object.create(Point.prototype) - // Wrap the Path constructor so objects can log - shorthand.Path = function () { - Path.apply(this, [true]) - Object.defineProperty(this, 'log', { value: self.context.store.log }) - } - shorthand.Path.prototype = Object.create(Path.prototype) - // Wrap the Snippet constructor so objects can log - shorthand.Snippet = function (def, anchor) { - Snippet.apply(this, [def, anchor, true]) - Snippet.apply(this, arguments) - Object.defineProperty(this, 'log', { value: self.context.store.log }) - } - shorthand.Snippet.prototype = Object.create(Snippet.prototype) - - // Proxy the points object - const pointsProxy = { - get: function () { - return Reflect.get(...arguments) - }, - set: (points, name, value) => { - // Constructor checks - if (value instanceof Point !== true) - self.context.store.log.warning( - `\`points.${name}\` was set with a value that is not a \`Point\` object` - ) - if (value.x == null || !utils.isCoord(value.x)) - self.context.store.log.warning( - `\`points.${name}\` was set with a \`x\` parameter that is not a \`number\`` - ) - if (value.y == null || !utils.isCoord(value.y)) - self.context.store.log.warning( - `\`points.${name}\` was set with a \`y\` parameter that is not a \`number\`` - ) - try { - value.name = name - } catch (err) { - self.context.store.log.warning(`Could not set \`name\` property on \`points.${name}\``) - } - return (self.points[name] = value) - }, - } - shorthand.points = new Proxy(this.points || {}, pointsProxy) - // Proxy the paths object - const pathsProxy = { - get: function () { - return Reflect.get(...arguments) - }, - set: (paths, name, value) => { - // Constructor checks - if (value instanceof Path !== true) - self.context.store.log.warning( - `\`paths.${name}\` was set with a value that is not a \`Path\` object` - ) - try { - value.name = name - } catch (err) { - self.context.store.log.warning(`Could not set \`name\` property on \`paths.${name}\``) - } - return (self.paths[name] = value) - }, - } - shorthand.paths = new Proxy(this.paths || {}, pathsProxy) - // Proxy the snippets object - const snippetsProxy = { - get: function (...args) { - return Reflect.get(...args) - }, - set: (snippets, name, value) => { - // Constructor checks - if (value instanceof Snippet !== true) - self.context.store.log.warning( - `\`snippets.${name}\` was set with a value that is not a \`Snippet\` object` - ) - if (typeof value.def !== 'string') - self.context.store.log.warning( - `\`snippets.${name}\` was set with a \`def\` parameter that is not a \`string\`` - ) - if (value.anchor instanceof Point !== true) - self.context.store.log.warning( - `\`snippets.${name}\` was set with an \`anchor\` parameter that is not a \`Point\`` - ) - try { - value.name = name - } catch (err) { - self.context.store.log.warning(`Could not set \`name\` property on \`snippets.${name}\``) - } - return (self.snippets[name] = value) - }, - } - shorthand.snippets = new Proxy(this.snippets || {}, snippetsProxy) - // Proxy the measurements object - const measurementsProxy = { - get: function (measurements, name) { - if (typeof measurements[name] === 'undefined') - self.context.store.log.warning( - `Tried to access \`measurements.${name}\` but it is \`undefined\`` - ) - return Reflect.get(...arguments) - }, - set: (measurements, name, value) => (self.context.settings.measurements[name] = value), - } - shorthand.measurements = new Proxy(this.context.settings.measurements || {}, measurementsProxy) - // Proxy the options object - const optionsProxy = { - get: function (options, name) { - if (typeof options[name] === 'undefined') - self.context.store.log.warning( - `Tried to access \`options.${name}\` but it is \`undefined\`` - ) - return Reflect.get(...arguments) - }, - set: (options, name, value) => (self.context.settings.options[name] = value), - } - shorthand.options = new Proxy(this.context.settings.options || {}, optionsProxy) - // Proxy the absoluteOptions object - const absoluteOptionsProxy = { - get: function (absoluteOptions, name) { - if (typeof absoluteOptions[name] === 'undefined') - self.context.store.log.warning( - `Tried to access \`absoluteOptions.${name}\` but it is \`undefined\`` - ) - return Reflect.get(...arguments) - }, - set: (absoluteOptions, name, value) => (self.context.settings.absoluteOptions[name] = value), - } - shorthand.absoluteOptions = new Proxy( - this.context.settings.absoluteOptions || {}, - absoluteOptionsProxy - ) - - return shorthand + return method } -//Part.prototype.isEmpty = function () { -// if (Object.keys(this.snippets).length > 0) return false -// -// if (Object.keys(this.paths).length > 0) { -// for (const p in this.paths) { -// if (this.paths[p].render && this.paths[p].length()) return false -// } -// } -// -// for (const p in this.points) { -// if (this.points[p].attributes.get('data-text')) return false -// if (this.points[p].attributes.get('data-circle')) return false -// } -// -// return true -//} +/** + * Returns a method to format values in the units provided in settings + * + * @private + * @return {function} method - The closured units method + */ +Part.prototype.__unitsClosure = function () { + const self = this + const method = function (value) { + if (typeof value !== 'number') + self.context.store.log.warning( + `Calling \`units(value)\` but \`value\` is not a number (\`${typeof value}\`)` + ) + return utils.units(value, self.context.settings.units) + } -export default Part + return method +} diff --git a/packages/core/src/path.mjs b/packages/core/src/path.mjs index 867f69d9413..bcd7613a322 100644 --- a/packages/core/src/path.mjs +++ b/packages/core/src/path.mjs @@ -9,17 +9,20 @@ import { pointOnCurve, curveEdge, round, - addNonEnumProp, + __addNonEnumProp, } from './utils.mjs' -export function Path(debug = false) { - // Non-enumerable properties - //addNonEnumProp(this, 'topLeft', false) - //addNonEnumProp(this, 'bottomRight', false) - //addNonEnumProp(this, 'attributes', new Attributes()) - //addNonEnumProp(this, 'ops', []) - addNonEnumProp(this, 'debug', debug) // FIXME: Is this needed? +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// +/** + * Constructor for a Path + * + * @constructor + * @return {Path} this - The Path instance + */ +export function Path() { // Enumerable properties this.hide = false this.ops = [] @@ -27,62 +30,21 @@ export function Path(debug = false) { this.topLeft = false this.bottomRight = false this.render = true -} - -/** Adds the log method for a path not created through the proxy **/ -Path.prototype.withLog = function (log = false) { - if (log) addNonEnumProp(this, 'log', log) return this } -/** Chainable way to set the render property */ -Path.prototype.setRender = function (render = true) { - if (render) this.render = true - else this.render = false +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// - return this -} - -/** Chainable way to set the class property */ -Path.prototype.setClass = function (className = false) { - if (className) this.attributes.set('class', className) - - return this -} - -/** Adds a move operation to Point to */ -Path.prototype.move = function (to) { - if (to instanceof Point !== true) - this.log.warning('Called `Path.move(to)` but `to` is not a `Point` object') - this.ops.push({ type: 'move', to }) - - return this -} - -/** Adds a line operation to Point to */ -Path.prototype.line = function (to) { - if (to instanceof Point !== true) - this.log.warning('Called `Path.line(to)` but `to` is not a `Point` object') - this.ops.push({ type: 'line', to }) - - return this -} - -/** Adds a curve operation via cp1 & cp2 to Point to */ -Path.prototype.curve = function (cp1, cp2, to) { - if (to instanceof Point !== true) - this.log.warning('Called `Path.curve(cp1, cp2, to)` but `to` is not a `Point` object') - if (cp1 instanceof Point !== true) - this.log.warning('Called `Path.curve(cp1, cp2, to)` but `cp1` is not a `Point` object') - if (cp2 instanceof Point !== true) - this.log.warning('Called `Path.curve(cp1, cp2, to)` but `cp2` is not a `Point` object') - this.ops.push({ type: 'curve', cp1, cp2, to }) - - return this -} - -/** Adds a curve operation without cp1 via cp2 to Point to */ +/** + * Adds a curve operation without cp1 via cp2 to Point to + * + * @param {Point} cp2 - The end control Point + * @param {Point} to - The end point + * @return {Path} this - The Path instance + */ Path.prototype._curve = function (cp2, to) { if (to instanceof Point !== true) this.log.warning('Called `Path._curve(cp2, to)` but `to` is not a `Point` object') @@ -94,70 +56,23 @@ Path.prototype._curve = function (cp2, to) { return this } -/** Adds a curve operation via cp1 with no cp2 to Point to */ -Path.prototype.curve_ = function (cp1, to) { - if (to instanceof Point !== true) - this.log.warning('Called `Path.curve_(cp1, to)` but `to` is not a `Point` object') - if (cp1 instanceof Point !== true) - this.log.warning('Called `Path.curve_(cp1, to)` but `cp1` is not a `Point` object') - let cp2 = to.copy() - this.ops.push({ type: 'curve', cp1, cp2, to }) +/** + * Chainable way to add to the class attribute + * + * @param {string} className - The value to add to the class attribute + * @return {Path} this - The Path instance + */ +Path.prototype.addClass = function (className = false) { + if (className) this.attributes.add('class', className) return this } -/** Adds a close operation */ -Path.prototype.close = function () { - this.ops.push({ type: 'close' }) - - return this -} - -/** Adds a noop operation */ -Path.prototype.noop = function (id = false) { - this.ops.push({ type: 'noop', id }) - - return this -} - -/** Replace a noop operation with the ops from path */ -Path.prototype.insop = function (noopId, path) { - if (!noopId) - this.log.warning('Called `Path.insop(noopId, path)` but `noopId` is undefined or false') - if (path instanceof Path !== true) - this.log.warning('Called `Path.insop(noopId, path) but `path` is not a `Path` object') - let newPath = this.clone() - for (let i in newPath.ops) { - if (newPath.ops[i].type === 'noop' && newPath.ops[i].id === noopId) { - newPath.ops = newPath.ops - .slice(0, i) - .concat(path.ops) - .concat(newPath.ops.slice(Number(i) + 1)) - } - } - - return newPath -} - -/** Adds an attribute. This is here to make this call chainable in assignment */ -Path.prototype.attr = function (name, value, overwrite = false) { - if (!name) - this.log.warning( - 'Called `Path.attr(name, value, overwrite=false)` but `name` is undefined or false' - ) - if (typeof value === 'undefined') - this.log.warning('Called `Path.attr(name, value, overwrite=false)` but `value` is undefined') - if (overwrite) - this.log.debug( - `Overwriting \`Path.attribute.${name}\` with ${value} (was: ${this.attributes.get(name)})` - ) - if (overwrite) this.attributes.set(name, value) - else this.attributes.add(name, value) - - return this -} - -/** Returns SVG pathstring for this path */ +/** + * Returns the SVG pathstring for this path + * + * @return {string} svg - The SVG pathsstring (the 'd' attribute of an SVG path) + */ Path.prototype.asPathstring = function () { let d = '' for (let op of this.ops) { @@ -182,14 +97,353 @@ Path.prototype.asPathstring = function () { return d } -/** Returns offset of this path as a new path */ -Path.prototype.offset = function (distance) { - if (typeof distance !== 'number') - this.log.error('Called `Path.offset(distance)` but `distance` is not a number') - return pathOffset(this, distance, this.log) +/** + * Chainable way to add an attribute + * + * @param {string} name - Name of the attribute to add + * @param {string} value - Value of the attribute to add + * @param {bool} overwrite - Whether to overwrite an existing attrubute or not + * @return {Path} this - The Path instance + */ +Path.prototype.attr = function (name, value, overwrite = false) { + if (!name) + this.log.warning( + 'Called `Path.attr(name, value, overwrite=false)` but `name` is undefined or false' + ) + if (typeof value === 'undefined') + this.log.warning('Called `Path.attr(name, value, overwrite=false)` but `value` is undefined') + if (overwrite) + this.log.debug( + `Overwriting \`Path.attribute.${name}\` with ${value} (was: ${this.attributes.get(name)})` + ) + if (overwrite) this.attributes.set(name, value) + else this.attributes.add(name, value) + + return this } -/** Returns the length of this path */ +/** + * Returns an object holding topLeft and bottomRight Points of the bounding box of this path + * + * @return {object} bbox - The bounding box object holding a topLeft and bottomRight Point instance + */ +Path.prototype.bbox = function () { + let bbs = [] + let current + for (let i in this.ops) { + let op = this.ops[i] + if (op.type === 'line') { + bbs.push(__lineBoundingBox({ from: current, to: op.to })) + } else if (op.type === 'curve') { + bbs.push( + __curveBoundingBox( + new Bezier( + { x: current.x, y: current.y }, + { x: op.cp1.x, y: op.cp1.y }, + { x: op.cp2.x, y: op.cp2.y }, + { x: op.to.x, y: op.to.y } + ) + ) + ) + } + if (op.to) current = op.to + } + + return __bbbbox(bbs) +} + +/** + * Returns a deep copy of this path + * + * @return {Path} clone - A clone of this Path instance + */ +Path.prototype.clone = function () { + let clone = new Path().__withLog(this.log).setRender(this.render) + if (this.topLeft) clone.topLeft = this.topLeft.clone() + else clone.topLeft = false + if (this.bottomRight) clone.bottomRight = this.bottomRight.clone() + else clone.bottomRight = false + clone.attributes = this.attributes.clone() + clone.ops = [] + for (let i in this.ops) { + let op = this.ops[i] + clone.ops[i] = { type: op.type } + if (op.type === 'move' || op.type === 'line') { + clone.ops[i].to = op.to.clone() + } else if (op.type === 'curve') { + clone.ops[i].to = op.to.clone() + clone.ops[i].cp1 = op.cp1.clone() + clone.ops[i].cp2 = op.cp2.clone() + } else if (op.type === 'noop') { + clone.ops[i].id = op.id + } + } + + return clone +} + +/** + * Adds a close operation + * + * @return {Path} this - The Path instance + */ +Path.prototype.close = function () { + this.ops.push({ type: 'close' }) + + return this +} + +/** + * Adds a curve operation via cp1 & cp2 to Point to + * + * @param {Point} cp1 - The start control Point + * @param {Point} cp2 - The end control Point + * @param {Point} to - The end point + * @return {Path} this - The Path instance + */ +Path.prototype.curve = function (cp1, cp2, to) { + if (to instanceof Point !== true) + this.log.warning('Called `Path.curve(cp1, cp2, to)` but `to` is not a `Point` object') + if (cp1 instanceof Point !== true) + this.log.warning('Called `Path.curve(cp1, cp2, to)` but `cp1` is not a `Point` object') + if (cp2 instanceof Point !== true) + this.log.warning('Called `Path.curve(cp1, cp2, to)` but `cp2` is not a `Point` object') + this.ops.push({ type: 'curve', cp1, cp2, to }) + + return this +} + +/** + * Adds a curve operation via cp1 with no cp2 to Point to + * + * @param {Point} cp1 - The start control Point + * @param {Point} to - The end point + * @return {Path} this - The Path instance + */ +Path.prototype.curve_ = function (cp1, to) { + if (to instanceof Point !== true) + this.log.warning('Called `Path.curve_(cp1, to)` but `to` is not a `Point` object') + if (cp1 instanceof Point !== true) + this.log.warning('Called `Path.curve_(cp1, to)` but `cp1` is not a `Point` object') + let cp2 = to.copy() + this.ops.push({ type: 'curve', cp1, cp2, to }) + + return this +} + +/** + * Divides this Path in atomic paths + * + * @return {Array} paths - An array of atomic paths that together make up this Path + */ +Path.prototype.divide = function () { + let paths = [] + let current, start + for (let i in this.ops) { + let op = this.ops[i] + if (op.type === 'move') { + start = op.to + } else if (op.type === 'line') { + if (!op.to.sitsRoughlyOn(current)) + paths.push(new Path().__withLog(this.log).move(current).line(op.to)) + } else if (op.type === 'curve') { + paths.push(new Path().__withLog(this.log).move(current).curve(op.cp1, op.cp2, op.to)) + } else if (op.type === 'close') { + paths.push(new Path().__withLog(this.log).move(current).line(start)) + } + if (op.to) current = op.to + } + + return paths +} + +/** + * Returns the point at an edge of this Path + * + * @param {string} side - One of 'topLeft', 'bottomRight', 'topRight', or 'bottomLeft' + * @return {object} point - The Point at the requested edge of (the bounding box of) this Path + */ +Path.prototype.edge = function (side) { + this.__boundary() + if (side === 'topLeft') return this.topLeft + else if (side === 'bottomRight') return this.bottomRight + else if (side === 'topRight') return new Point(this.bottomRight.x, this.topLeft.y) + else if (side === 'bottomLeft') return new Point(this.topLeft.x, this.bottomRight.y) + else { + let s = side + 'Op' + if (this[s].type === 'move') return this[s].to + else if (this[s].type === 'line') { + if (side === 'top') { + if (this.topOp.to.y < this.topOp.from.y) return this.topOp.to + else return this.topOp.from + } else if (side === 'left') { + if (this.leftOp.to.x < this.leftOp.from.x) return this.leftOp.to + else return this.leftOp.from + } else if (side === 'bottom') { + if (this.bottomOp.to.y > this.bottomOp.from.y) return this.bottomOp.to + else return this.bottomOp.from + } else if (side === 'right') { + if (this.rightOp.to.x > this.rightOp.from.x) return this.rightOp.to + else return this.rightOp.from + } + } else if (this[s].type === 'curve') + return curveEdge( + new Bezier( + { x: this[s].from.x, y: this[s].from.y }, + { x: this[s].cp1.x, y: this[s].cp1.y }, + { x: this[s].cp2.x, y: this[s].cp2.y }, + { x: this[s].to.x, y: this[s].to.y } + ), + side + ) + } +} + +/** + * Returns the endpoint of this path + * + * @return {Point} end - The end point + */ +Path.prototype.end = function () { + if (this.ops.length < 1) + this.log.error('Called `Path.end()` but this path has no drawing operations') + let op = this.ops[this.ops.length - 1] + + if (op.type === 'close') return this.start() + else return op.to +} + +/** + * Replace a noop operation with the ops from path + * + * @param {string} noopId = The ID of the noop where the operations should be inserted + * @param {Path} path = The path of which the operations should be inserted + * @return {object} this - The Path instance + */ +Path.prototype.insop = function (noopId, path) { + if (!noopId) + this.log.warning('Called `Path.insop(noopId, path)` but `noopId` is undefined or false') + if (path instanceof Path !== true) + this.log.warning('Called `Path.insop(noopId, path) but `path` is not a `Path` object') + let newPath = this.clone() + for (let i in newPath.ops) { + if (newPath.ops[i].type === 'noop' && newPath.ops[i].id === noopId) { + newPath.ops = newPath.ops + .slice(0, i) + .concat(path.ops) + .concat(newPath.ops.slice(Number(i) + 1)) + } + } + + return newPath +} + +/** + * Finds intersections between this Path and another Path + * + * @param {Path} path - The Path instance to check for intersections with this Path instance + * @return {Array} intersections - An array of Point objects where the paths intersect + */ +Path.prototype.intersects = function (path) { + if (this === path) + this.log.error('You called Path.intersects(path)` but `path` and `this` are the same object') + let intersections = [] + for (let pathA of this.divide()) { + for (let pathB of path.divide()) { + if (pathA.ops[1].type === 'line') { + if (pathB.ops[1].type === 'line') { + __addIntersectionsToArray( + linesIntersect(pathA.ops[0].to, pathA.ops[1].to, pathB.ops[0].to, pathB.ops[1].to), + intersections + ) + } else if (pathB.ops[1].type === 'curve') { + __addIntersectionsToArray( + lineIntersectsCurve( + pathA.ops[0].to, + pathA.ops[1].to, + pathB.ops[0].to, + pathB.ops[1].cp1, + pathB.ops[1].cp2, + pathB.ops[1].to + ), + intersections + ) + } + } else if (pathA.ops[1].type === 'curve') { + if (pathB.ops[1].type === 'line') { + __addIntersectionsToArray( + lineIntersectsCurve( + pathB.ops[0].to, + pathB.ops[1].to, + pathA.ops[0].to, + pathA.ops[1].cp1, + pathA.ops[1].cp2, + pathA.ops[1].to + ), + intersections + ) + } else if (pathB.ops[1].type === 'curve') { + __addIntersectionsToArray( + curvesIntersect( + pathA.ops[0].to, + pathA.ops[1].cp1, + pathA.ops[1].cp2, + pathA.ops[1].to, + pathB.ops[0].to, + pathB.ops[1].cp1, + pathB.ops[1].cp2, + pathB.ops[1].to + ), + intersections + ) + } + } + } + } + + return intersections +} + +/** + * Finds intersections between this Path and an X value + * + * @param {float} x - The X-value to check for intersections + * @return {Array} paths - An array of atomic paths that together make up this Path + */ +Path.prototype.intersectsX = function (x) { + if (typeof x !== 'number') this.log.error('Called `Path.intersectsX(x)` but `x` is not a number') + return this.__intersectsAxis(x, 'x') +} + +/** + * Finds intersections between this Path and an Y value + * + * @param {float} y - The Y-value to check for intersections + * @return {Array} paths - An array of atomic paths that together make up this Path + */ +Path.prototype.intersectsY = function (y) { + if (typeof y !== 'number') this.log.error('Called `Path.intersectsX(y)` but `y` is not a number') + return this.__intersectsAxis(y, 'y') +} + +/** + * Joins this Path with that Path, and closes them if wanted + * + * @param {Path} that - The Path to join this Path with + * @param {bool} closed - Whether or not to close the joint Path + * @return {Path} joint - The joint Path instance + */ +Path.prototype.join = function (that, closed = false) { + if (that instanceof Path !== true) + this.log.error('Called `Path.join(that)` but `that` is not a `Path` object') + return __joinPaths([this, that], closed, this.log) +} + +/** + * Return the length of this Path + * + * @return {float} length - The length of this path + */ Path.prototype.length = function () { let current, start let length = 0 @@ -215,25 +469,381 @@ Path.prototype.length = function () { return length } -/** Returns the startpoint of the path */ +/** + * Adds a line operation to Point to + * + * @param {Point} to - The point to stroke to + * @return {object} this - The Path instance + */ +Path.prototype.line = function (to) { + if (to instanceof Point !== true) + this.log.warning('Called `Path.line(to)` but `to` is not a `Point` object') + this.ops.push({ type: 'line', to }) + + return this +} + +/** + * Adds a move operation to Point to + * + * @param {Point} to - The point to move to + * @return {object} this - The Path instance + */ +Path.prototype.move = function (to) { + if (to instanceof Point !== true) + this.log.warning('Called `Path.move(to)` but `to` is not a `Point` object') + this.ops.push({ type: 'move', to }) + + return this +} + +/** + * Adds a noop operation + * + * @param {string} id = The ID to reference this noop later with Path.insop() + * @return {object} this - The Path instance + */ +Path.prototype.noop = function (id = false) { + this.ops.push({ type: 'noop', id }) + + return this +} + +/** + * Returns an offset version of this path as a new path + * + * @param {float} distance - The distance by which to offset + * @return {object} this - The Path instance + */ +Path.prototype.offset = function (distance) { + if (typeof distance !== 'number') + this.log.error('Called `Path.offset(distance)` but `distance` is not a number') + return __pathOffset(this, distance, this.log) +} + +/** + * Returns a reversed version of this Path + * + * @return {object} reverse - A Path instance that is the reversed version of this Path + */ +Path.prototype.reverse = function () { + let sections = [] + let current + let closed = false + for (let i in this.ops) { + let op = this.ops[i] + if (op.type === 'line') { + if (!op.to.sitsOn(current)) + sections.push(new Path().__withLog(this.log).move(op.to).line(current)) + } else if (op.type === 'curve') { + sections.push(new Path().__withLog(this.log).move(op.to).curve(op.cp2, op.cp1, current)) + } else if (op.type === 'close') { + closed = true + } + if (op.to) current = op.to + } + let rev = new Path().__withLog(this.log).move(current) + for (let section of sections.reverse()) rev.ops.push(section.ops[1]) + if (closed) rev.close() + + return rev +} + +/** + * Returns a rough estimate of the length of this path + * + * This avoids walking Bezier curves and thus is much faster but not accurate at all + * + * @return {float} length - The approximate length of the path + */ +Path.prototype.roughLength = function () { + let current, start + let length = 0 + for (let i in this.ops) { + let op = this.ops[i] + if (op.type === 'move') { + start = op.to + } else if (op.type === 'line') { + length += current.dist(op.to) + } else if (op.type === 'curve') { + length += current.dist(op.cp1) + length += op.cp1.dist(op.cp2) + length += op.cp2.dist(op.to) + } else if (op.type === 'close') { + length += current.dist(start) + } + if (op.to) current = op.to + } + + return length +} + +/** + * Chainable way to set the class attribute + * + * @param {string} className - The value to set on the class attribute + * @return {object} this - The Path instance + */ +Path.prototype.setClass = function (className = false) { + if (className) this.attributes.set('class', className) + + return this +} + +/** FIXME: This should go */ +Path.prototype.setRender = function (render = true) { + if (render) this.render = true + else this.render = false + + return this +} + +/** + * Returns a point that lies at distance along this Path + * + * @param {float} distance - The distance to shift along this Path + * @param {int} stepsPerMm - The amount of steps per millimeter to talke while walking the cubic Bezier curve + * @return {Point} point - The point that lies distance along this Path + */ +Path.prototype.shiftAlong = function (distance, stepsPerMm = 10) { + if (typeof distance !== 'number') + this.log.error('Called `Path.shiftAlong(distance)` but `distance` is not a number') + let len = 0 + let current + for (let i in this.ops) { + let op = this.ops[i] + if (op.type === 'line') { + let thisLen = op.to.dist(current) + if (Math.abs(len + thisLen - distance) < 0.1) return op.to + if (len + thisLen > distance) return current.shiftTowards(op.to, distance - len) + len += thisLen + } else if (op.type === 'curve') { + let bezier = new Bezier( + { x: current.x, y: current.y }, + { x: op.cp1.x, y: op.cp1.y }, + { x: op.cp2.x, y: op.cp2.y }, + { x: op.to.x, y: op.to.y } + ) + let thisLen = bezier.length() + if (Math.abs(len + thisLen - distance) < 0.1) return op.to + if (len + thisLen > distance) + return shiftAlongBezier(distance - len, bezier, thisLen * stepsPerMm) + len += thisLen + } + current = op.to + } + this.log.error( + `Called \`Path.shiftAlong(distance)\` with a \`distance\` of \`${distance}\` but \`Path.length()\` is only \`${this.length()}\`` + ) +} + +/** + * Returns a point that lies at fraction along this Path + * + * @param {float} fraction - The fraction to shift along this Path + * @param {int} stepsPerMm - The amount of steps per millimeter to talke while walking the cubic Bezier curve + * @return {Point} point - The point that lies fraction along this Path + */ +Path.prototype.shiftFractionAlong = function (fraction, stepsPerMm = 10) { + if (typeof fraction !== 'number') + this.log.error('Called `Path.shiftFractionAlong(fraction)` but `fraction` is not a number') + return this.shiftAlong(this.length() * fraction, stepsPerMm) +} + +/** + * Splits path on point, and retuns both halves as Path instances + * + * @param {Point} point - The Point to split this Path on + * @return {Array} halves - An array holding the two Path instances that make the split halves + */ +Path.prototype.split = function (point) { + if (point instanceof Point !== true) + this.log.error('Called `Path.split(point)` but `point` is not a `Point` object') + let divided = this.divide() + let firstHalf = [] + let secondHalf = [] + for (let pi = 0; pi < divided.length; pi++) { + let path = divided[pi] + if (path.ops[1].type === 'line') { + if (path.ops[0].to.sitsRoughlyOn(point)) { + secondHalf.push(new Path().__withLog(this.log).move(path.ops[0].to).line(path.ops[1].to)) + } else if (path.ops[1].to.sitsRoughlyOn(point)) { + firstHalf.push(new Path().__withLog(this.log).move(path.ops[0].to).line(path.ops[1].to)) + } else if (pointOnLine(path.ops[0].to, path.ops[1].to, point)) { + firstHalf = divided.slice(0, pi) + firstHalf.push(new Path().__withLog(this.log).move(path.ops[0].to).line(point)) + pi++ + secondHalf = divided.slice(pi) + secondHalf.unshift(new Path().__withLog(this.log).move(point).line(path.ops[1].to)) + } + } else if (path.ops[1].type === 'curve') { + if (path.ops[0].to.sitsRoughlyOn(point)) { + secondHalf.push( + new Path() + .__withLog(this.log) + .move(path.ops[0].to) + .curve(path.ops[1].cp1, path.ops[1].cp2, path.ops[1].to) + ) + } else if (path.ops[1].to.sitsRoughlyOn(point)) { + firstHalf.push( + new Path() + .__withLog(this.log) + .move(path.ops[0].to) + .curve(path.ops[1].cp1, path.ops[1].cp2, path.ops[1].to) + ) + } else { + let t = pointOnCurve( + path.ops[0].to, + path.ops[1].cp1, + path.ops[1].cp2, + path.ops[1].to, + point + ) + if (t !== false) { + let curve = new Bezier( + { x: path.ops[0].to.x, y: path.ops[0].to.y }, + { x: path.ops[1].cp1.x, y: path.ops[1].cp1.y }, + { x: path.ops[1].cp2.x, y: path.ops[1].cp2.y }, + { x: path.ops[1].to.x, y: path.ops[1].to.y } + ) + let split = curve.split(t) + firstHalf = divided.slice(0, pi) + firstHalf.push( + new Path() + .__withLog(this.log) + .move(new Point(split.left.points[0].x, split.left.points[0].y)) + .curve( + new Point(split.left.points[1].x, split.left.points[1].y), + new Point(split.left.points[2].x, split.left.points[2].y), + new Point(split.left.points[3].x, split.left.points[3].y) + ) + ) + pi++ + secondHalf = divided.slice(pi) + secondHalf.unshift( + new Path() + .__withLog(this.log) + .move(new Point(split.right.points[0].x, split.right.points[0].y)) + .curve( + new Point(split.right.points[1].x, split.right.points[1].y), + new Point(split.right.points[2].x, split.right.points[2].y), + new Point(split.right.points[3].x, split.right.points[3].y) + ) + ) + } + } + } + } + if (firstHalf.length > 0) firstHalf = __joinPaths(firstHalf, false, this.log) + if (secondHalf.length > 0) secondHalf = __joinPaths(secondHalf, false, this.log) + + return [firstHalf, secondHalf] +} + +/** + * Returns the startpoint of this path + * + * @return {Point} start - The start point + */ Path.prototype.start = function () { if (this.ops.length < 1 || typeof this.ops[0].to === 'undefined') this.log.error('Called `Path.start()` but this path has no drawing operations') return this.ops[0].to } -/** Returns the endpoint of the path */ -Path.prototype.end = function () { - if (this.ops.length < 1) - this.log.error('Called `Path.end()` but this path has no drawing operations') - let op = this.ops[this.ops.length - 1] +/** + * Returns a cloned Path instance with a translate tranform applied + * + * @param {float} x - The X-value for the transform + * @param {float} y - The Y-value for the transform + * @return {Path} this - This Path instance + */ +Path.prototype.translate = function (x, y) { + if (typeof x !== 'number') + this.log.warning('Called `Path.translate(x, y)` but `x` is not a number') + if (typeof y !== 'number') + this.log.warning('Called `Path.translate(x, y)` but `y` is not a number') + let clone = this.clone() + for (let op of clone.ops) { + if (op.type !== 'close') { + op.to = op.to.translate(x, y) + } + if (op.type === 'curve') { + op.cp1 = op.cp1.translate(x, y) + op.cp2 = op.cp2.translate(x, y) + } + } - if (op.type === 'close') return this.start() - else return op.to + return clone } -/** Finds the bounding box of a path */ -Path.prototype.boundary = function () { +/** + * Removes self-intersections (overlap) from this Path instance + * + * @return {Path} this - This Path instance + */ +Path.prototype.trim = function () { + let chunks = this.divide() + for (let i = 0; i < chunks.length; i++) { + let firstCandidate = parseInt(i) + 2 + let lastCandidate = parseInt(chunks.length) - 1 + for (let j = firstCandidate; j < lastCandidate; j++) { + let intersections = chunks[i].intersects(chunks[j]) + if (intersections.length > 0) { + let intersection = intersections.pop() + let trimmedStart = chunks.slice(0, i) + let trimmedEnd = chunks.slice(parseInt(j) + 1) + let glue = new Path().__withLog(this.log) + let first = true + for (let k of [i, j]) { + let ops = chunks[k].ops + if (ops[1].type === 'line') { + glue.line(intersection) + } else if (ops[1].type === 'curve') { + // handle curve + let curve = new Bezier( + { x: ops[0].to.x, y: ops[0].to.y }, + { x: ops[1].cp1.x, y: ops[1].cp1.y }, + { x: ops[1].cp2.x, y: ops[1].cp2.y }, + { x: ops[1].to.x, y: ops[1].to.y } + ) + let t = pointOnCurve(ops[0].to, ops[1].cp1, ops[1].cp2, ops[1].to, intersection) + let split = curve.split(t) + let side + if (first) side = split.left + else side = split.right + glue.curve( + new Point(side.points[1].x, side.points[1].y), + new Point(side.points[2].x, side.points[2].y), + new Point(side.points[3].x, side.points[3].y) + ) + } + first = false + } + let joint + if (trimmedStart.length > 0) joint = __joinPaths(trimmedStart, false, this.log).join(glue) + else joint = glue + if (trimmedEnd.length > 0) joint = joint.join(__joinPaths(trimmedEnd, false, this.log)) + + return joint.trim() + } + } + } + + return this +} + +////////////////////////////////////////////// +// PRIVATE METHODS // +////////////////////////////////////////////// + +/** + * Finds the bounding box of a path + * + * @private + * @return {object} this - The Path instance + */ +Path.prototype.__boundary = function () { if (this.topOp) return this // Cached let current @@ -298,92 +908,121 @@ Path.prototype.boundary = function () { return this } -/** Returns a deep copy of this */ -Path.prototype.clone = function () { - let clone = new Path(this.debug).withLog(this.log).setRender(this.render) - if (this.topLeft) clone.topLeft = this.topLeft.clone() - else clone.topLeft = false - if (this.bottomRight) clone.bottomRight = this.bottomRight.clone() - else clone.bottomRight = false - clone.attributes = this.attributes.clone() - clone.ops = [] - for (let i in this.ops) { - let op = this.ops[i] - clone.ops[i] = { type: op.type } - if (op.type === 'move' || op.type === 'line') { - clone.ops[i].to = op.to.clone() - } else if (op.type === 'curve') { - clone.ops[i].to = op.to.clone() - clone.ops[i].cp1 = op.cp1.clone() - clone.ops[i].cp2 = op.cp2.clone() - } else if (op.type === 'noop') { - clone.ops[i].id = op.id +/** + * Finds intersections between this Path and a X or Y value + * + * @private + * @param {float} val - The X or Y value check for intersections + * @param {string} mode - Either 'x' or 'y' to indicate to check for intersections on the X or Y axis + * @return {Array} intersections - An array of Point objects where the Path intersects + */ +Path.prototype.__intersectsAxis = function (val = false, mode) { + let intersections = [] + let lineStart = mode === 'x' ? new Point(val, -100000) : new Point(-10000, val) + let lineEnd = mode === 'x' ? new Point(val, 100000) : new Point(100000, val) + for (let path of this.divide()) { + if (path.ops[1].type === 'line') { + __addIntersectionsToArray( + linesIntersect(path.ops[0].to, path.ops[1].to, lineStart, lineEnd), + intersections + ) + } else if (path.ops[1].type === 'curve') { + __addIntersectionsToArray( + lineIntersectsCurve( + lineStart, + lineEnd, + path.ops[0].to, + path.ops[1].cp1, + path.ops[1].cp2, + path.ops[1].to + ), + intersections + ) } } - return clone + return intersections } -/** Joins this with that path, closes them if wanted */ -Path.prototype.join = function (that, closed = false) { - if (that instanceof Path !== true) - this.log.error('Called `Path.join(that)` but `that` is not a `Path` object') - return joinPaths([this, that], closed, this.log) +/** + * Adds the log method for a path not created through the proxy + * + * @private + * @return {object} this - The Path instance + */ +Path.prototype.__withLog = function (log = false) { + if (log) __addNonEnumProp(this, 'log', log) + + return this } -/** Offsets a path by distance */ -function pathOffset(path, distance, log) { - let offset = [] - let current - let start = false - let closed = false - for (let i in path.ops) { - let op = path.ops[i] - if (op.type === 'line') { - let segment = offsetLine(current, op.to, distance, path.debug, path.log) - if (segment) offset.push(segment) - } else if (op.type === 'curve') { - // We need to avoid a control point sitting on top of start or end - // because that will break the offset in bezier-js - let cp1, cp2 - if (current.sitsRoughlyOn(op.cp1)) { - cp1 = new Path(path.debug).withLog(path.log).move(current).curve(op.cp1, op.cp2, op.to) - cp1 = cp1.shiftAlong(cp1.length() > 2 ? 2 : cp1.length() / 10) - } else cp1 = op.cp1 - if (op.cp2.sitsRoughlyOn(op.to)) { - cp2 = new Path(path.debug).withLog(path.log).move(op.to).curve(op.cp2, op.cp1, current) - cp2 = cp2.shiftAlong(cp2.length() > 2 ? 2 : cp2.length() / 10) - } else cp2 = op.cp2 - let b = new Bezier( - { x: current.x, y: current.y }, - { x: cp1.x, y: cp1.y }, - { x: cp2.x, y: cp2.y }, - { x: op.to.x, y: op.to.y } - ) - for (let bezier of b.offset(distance)) offset.push(asPath(bezier, path.debug, path.log)) - } else if (op.type === 'close') closed = true - if (op.to) current = op.to - if (!start) start = current + + + +////////////////////////////////////////////// +// PUBLIC STATIC METHODS // +////////////////////////////////////////////// + +/** + * Returns a ready-to-proxy that logs when things aren't exactly ok + * + * @private + * @param {object} paths - The paths object to proxy + * @param {object} log - The logging object + * @return {object} proxy - The object that is ready to be proxied + */ +export function pathsProxy(paths, log) { + return { + get: function (...args) { + return Reflect.get(...args) + }, + set: (paths, name, value) => { + // Constructor checks + if (value instanceof Path !== true) + log.warning(`\`paths.${name}\` was set with a value that is not a \`Path\` object`) + try { + value.name = name + } catch (err) { + log.warning(`Could not set \`name\` property on \`paths.${name}\``) + } + return (paths[name] = value) + }, } - - return joinPaths(offset, closed, log) } -/** Offsets a line by distance */ -function offsetLine(from, to, distance, debug = false, log = false) { - if (from.x === to.x && from.y === to.y) return false - let angle = from.angle(to) - 90 +////////////////////////////////////////////// +// PRIVATE STATIC METHODS // +////////////////////////////////////////////// - return new Path(debug) - .withLog(log) - .move(from.shift(angle, distance)) - .line(to.shift(angle, distance)) +/** + * Helper method to add intersection candidates to Array + * + * @private + * @param {Array|Object|false} candidates - One Point or an array of Points to check for intersection + * @param {Path} path - The Path instance to add as intersection if it has coordinates + * @return {Array} intersections - An array of Point objects where the paths intersect + */ +function __addIntersectionsToArray(candidates, intersections) { + if (!candidates) return + if (typeof candidates === 'object') { + if (typeof candidates.x === 'number') intersections.push(candidates) + else { + for (let candidate of candidates) intersections.push(candidate) + } + } } -/** Converts a bezier-js instance to a path */ -function asPath(bezier, debug = false, log = false) { - return new Path(debug) - .withLog(log) +/** + * Converts a bezier-js instance to a path + * + * @private + * @param {BezierJs} bezier - A BezierJs instance + * @param {object} log - The logging object + * @return {object} path - A Path instance + */ +function __asPath(bezier, log = false) { + return new Path() + .__withLog(log) .move(new Point(bezier.points[0].x, bezier.points[0].y)) .curve( new Point(bezier.points[1].x, bezier.points[1].y), @@ -392,9 +1031,54 @@ function asPath(bezier, debug = false, log = false) { ) } -/** Joins path segments together into one path */ -function joinPaths(paths, closed = false) { - let joint = new Path(paths[0].debug).withLog(paths[0].log).move(paths[0].ops[0].to) +/** + * Returns the bounding box of multiple bounding boxes + * + * @private + * @param {Array} boxes - An Array of bounding box objects + * @return {object} bbox - The bounding box object holding a topLeft and bottomRight Point instance + */ +function __bbbbox(boxes) { + let minX = Infinity + let maxX = -Infinity + let minY = Infinity + let maxY = -Infinity + for (let box of boxes) { + if (box.topLeft.x < minX) minX = box.topLeft.x + if (box.topLeft.y < minY) minY = box.topLeft.y + if (box.bottomRight.x > maxX) maxX = box.bottomRight.x + if (box.bottomRight.y > maxY) maxY = box.bottomRight.y + } + + return { topLeft: new Point(minX, minY), bottomRight: new Point(maxX, maxY) } +} + +/** + * Returns an object holding topLeft and bottomRight Points of the bounding box of a curve + * + * @private + * @param {BezierJs} curve - A BezierJs instance representing the curve + * @return {object} point - The bounding box object holding a topLeft and bottomRight Point instance + */ +function __curveBoundingBox(curve) { + let bb = curve.bbox() + + return { + topLeft: new Point(bb.x.min, bb.y.min), + bottomRight: new Point(bb.x.max, bb.y.max), + } +} + +/** + * Joins path segments together into one path + * + * @private + * @param {Array} paths - An Array of Path objects + * @param {bool} closed - Whether or not to close the joined paths + * @return {object} path - A Path instance + */ +function __joinPaths(paths, closed = false) { + let joint = new Path().__withLog(paths[0].log).move(paths[0].ops[0].to) let current for (let p of paths) { for (let op of p.ops) { @@ -416,90 +1100,14 @@ function joinPaths(paths, closed = false) { return joint } -/** Returns a point that lies at distance along this */ -Path.prototype.shiftAlong = function (distance, stepsPerMm = 25) { - if (typeof distance !== 'number') - this.log.error('Called `Path.shiftAlong(distance)` but `distance` is not a number') - let len = 0 - let current - for (let i in this.ops) { - let op = this.ops[i] - if (op.type === 'line') { - let thisLen = op.to.dist(current) - if (Math.abs(len + thisLen - distance) < 0.1) return op.to - if (len + thisLen > distance) return current.shiftTowards(op.to, distance - len) - len += thisLen - } else if (op.type === 'curve') { - let bezier = new Bezier( - { x: current.x, y: current.y }, - { x: op.cp1.x, y: op.cp1.y }, - { x: op.cp2.x, y: op.cp2.y }, - { x: op.to.x, y: op.to.y } - ) - let thisLen = bezier.length() - if (Math.abs(len + thisLen - distance) < 0.1) return op.to - if (len + thisLen > distance) - return shiftAlongBezier(distance - len, bezier, thisLen * stepsPerMm) - len += thisLen - } - current = op.to - } - this.log.error( - `Called \`Path.shiftAlong(distance)\` with a \`distance\` of \`${distance}\` but \`Path.length()\` is only \`${this.length()}\`` - ) -} - -/** Returns a point that lies at fraction along this */ -Path.prototype.shiftFractionAlong = function (fraction, stepsPerMm = 25) { - if (typeof fraction !== 'number') - this.log.error('Called `Path.shiftFractionAlong(fraction)` but `fraction` is not a number') - return this.shiftAlong(this.length() * fraction, stepsPerMm) -} - -/** Returns a point that lies at distance along bezier */ -function shiftAlongBezier(distance, bezier, steps = 100) { - let previous, next, t, thisLen - let len = 0 - for (let i = 0; i <= steps; i++) { - t = i / steps - next = bezier.get(t) - next = new Point(next.x, next.y) - if (i > 0) { - thisLen = next.dist(previous) - if (len + thisLen > distance) return next - else len += thisLen - } - previous = next - } -} - -/** Returns a point at the top edge of a bounding box of this */ -Path.prototype.bbox = function () { - let bbs = [] - let current - for (let i in this.ops) { - let op = this.ops[i] - if (op.type === 'line') { - bbs.push(lineBoundingBox({ from: current, to: op.to })) - } else if (op.type === 'curve') { - bbs.push( - curveBoundingBox( - new Bezier( - { x: current.x, y: current.y }, - { x: op.cp1.x, y: op.cp1.y }, - { x: op.cp2.x, y: op.cp2.y }, - { x: op.to.x, y: op.to.y } - ) - ) - ) - } - if (op.to) current = op.to - } - - return bbbbox(bbs) -} - -function lineBoundingBox(line) { +/** + * Returns an object holding topLeft and bottomRight Points of the bounding box of a line + * + * @private + * @param {object} line - An object with a from and to Point instance that represents a line + * @return {object} point - The bounding box object holding a topLeft and bottomRight Point instance + */ +function __lineBoundingBox(line) { let from = line.from let to = line.to if (from.x === to.x) { @@ -529,389 +1137,104 @@ function lineBoundingBox(line) { } } -function curveBoundingBox(curve) { - let bb = curve.bbox() +/** + * Offsets a line by distance + * + * @private + * @param {Point} from - The line's start point + * @param {Point} to - The line's end point + * @param {float} distance - The distane by which to offset the line + * @param {object} log - The logging object + * @return {object} this - The Path instance + */ +function __offsetLine(from, to, distance, log = false) { + if (from.x === to.x && from.y === to.y) return false + let angle = from.angle(to) - 90 - return { - topLeft: new Point(bb.x.min, bb.y.min), - bottomRight: new Point(bb.x.max, bb.y.max), - } + return new Path().__withLog(log).move(from.shift(angle, distance)).line(to.shift(angle, distance)) } -function bbbbox(boxes) { - let minX = Infinity - let maxX = -Infinity - let minY = Infinity - let maxY = -Infinity - for (let box of boxes) { - if (box.topLeft.x < minX) minX = box.topLeft.x - if (box.topLeft.y < minY) minY = box.topLeft.y - if (box.bottomRight.x > maxX) maxX = box.bottomRight.x - if (box.bottomRight.y > maxY) maxY = box.bottomRight.y - } - - return { topLeft: new Point(minX, minY), bottomRight: new Point(maxX, maxY) } -} - -/** Returns a reversed version of this */ -Path.prototype.reverse = function () { - let sections = [] +/** + * Offsets a path by distance + * + * @private + * @param {Path} path - The Path to offset + * @param {float} distance - The distance to offset by + * @param {object} log - The log methods + * @return {Path} offsetted - The offsetted Path instance + */ +function __pathOffset(path, distance, log) { + let offset = [] let current + let start = false let closed = false - for (let i in this.ops) { - let op = this.ops[i] + for (let i in path.ops) { + let op = path.ops[i] if (op.type === 'line') { - if (!op.to.sitsOn(current)) - sections.push(new Path(this.debug).withLog(this.log).move(op.to).line(current)) + let segment = __offsetLine(current, op.to, distance, path.log) + if (segment) offset.push(segment) } else if (op.type === 'curve') { - sections.push( - new Path(this.debug).withLog(this.log).move(op.to).curve(op.cp2, op.cp1, current) + // We need to avoid a control point sitting on top of start or end + // because that will break the offset in bezier-js + let cp1, cp2 + if (current.sitsRoughlyOn(op.cp1)) { + cp1 = new Path().__withLog(path.log).move(current).curve(op.cp1, op.cp2, op.to) + cp1 = cp1.shiftAlong(cp1.length() > 2 ? 2 : cp1.length() / 10) + } else cp1 = op.cp1 + if (op.cp2.sitsRoughlyOn(op.to)) { + cp2 = new Path().__withLog(path.log).move(op.to).curve(op.cp2, op.cp1, current) + cp2 = cp2.shiftAlong(cp2.length() > 2 ? 2 : cp2.length() / 10) + } else cp2 = op.cp2 + let b = new Bezier( + { x: current.x, y: current.y }, + { x: cp1.x, y: cp1.y }, + { x: cp2.x, y: cp2.y }, + { x: op.to.x, y: op.to.y } ) - } else if (op.type === 'close') { - closed = true - } + for (let bezier of b.offset(distance)) offset.push(__asPath(bezier, path.log)) + } else if (op.type === 'close') closed = true if (op.to) current = op.to + if (!start) start = current } - let rev = new Path(this.debug).withLog(this.log).move(current) - for (let section of sections.reverse()) rev.ops.push(section.ops[1]) - if (closed) rev.close() - return rev + return __joinPaths(offset, closed, log) } -/** Returns the point at an edge of this path */ -Path.prototype.edge = function (side) { - this.boundary() - if (side === 'topLeft') return this.topLeft - else if (side === 'bottomRight') return this.bottomRight - else if (side === 'topRight') return new Point(this.bottomRight.x, this.topLeft.y) - else if (side === 'bottomLeft') return new Point(this.topLeft.x, this.bottomRight.y) - else { - let s = side + 'Op' - if (this[s].type === 'move') return this[s].to - else if (this[s].type === 'line') { - if (side === 'top') { - if (this.topOp.to.y < this.topOp.from.y) return this.topOp.to - else return this.topOp.from - } else if (side === 'left') { - if (this.leftOp.to.x < this.leftOp.from.x) return this.leftOp.to - else return this.leftOp.from - } else if (side === 'bottom') { - if (this.bottomOp.to.y > this.bottomOp.from.y) return this.bottomOp.to - else return this.bottomOp.from - } else if (side === 'right') { - if (this.rightOp.to.x > this.rightOp.from.x) return this.rightOp.to - else return this.rightOp.from - } - } else if (this[s].type === 'curve') { - let curve = edgeCurveAsBezier(this[s]) - return curveEdge(curve, side) +/** + * Returns a Point that lies at distance along a cubic Bezier curve + * + * @private + * @param {float} distance - The distance to shift along the cubic Bezier curve + * @param {Bezier} bezier - The BezierJs instance + * @param {int} steps - The numer of steps to walk the Bezier with + * @return {Point} point - The point at distance along the cubic Bezier curve + */ +function shiftAlongBezier(distance, bezier, steps = false) { + let rlen + if (!steps) { + rlen = new Path() + .move(new Point(...Object.values(bezier.points[0]))) + .curve( + new Point(...Object.values(bezier.points[1])), + new Point(...Object.values(bezier.points[2])), + new Point(...Object.values(bezier.points[3])), + ).roughLength() + if (rlen < 2) steps = 20 + else if (rlen < 10) steps = 40 + else if (rlen < 100) steps = 100 + else steps = 200 + } + let previous, next, t, thisLen + let len = 0 + for (let i = 0; i <= steps; i++) { + t = i / steps + next = bezier.get(t) + next = new Point(next.x, next.y) + if (i > 0) { + thisLen = next.dist(previous) + if (len + thisLen > distance) return next + else len += thisLen } + previous = next } } - -function edgeCurveAsBezier(op) { - return new Bezier( - { x: op.from.x, y: op.from.y }, - { x: op.cp1.x, y: op.cp1.y }, - { x: op.cp2.x, y: op.cp2.y }, - { x: op.to.x, y: op.to.y } - ) -} - -/** Divides a path into atomic paths */ -Path.prototype.divide = function () { - let paths = [] - let current, start - for (let i in this.ops) { - let op = this.ops[i] - if (op.type === 'move') { - start = op.to - } else if (op.type === 'line') { - if (!op.to.sitsRoughlyOn(current)) - paths.push(new Path(this.debug).withLog(this.log).move(current).line(op.to)) - } else if (op.type === 'curve') { - paths.push(new Path(this.debug).withLog(this.log).move(current).curve(op.cp1, op.cp2, op.to)) - } else if (op.type === 'close') { - paths.push(new Path(this.debug).withLog(this.log).move(current).line(start)) - } - if (op.to) current = op.to - } - - return paths -} - -/** Finds intersections between this path and an X value */ -Path.prototype.intersectsX = function (x) { - if (typeof x !== 'number') this.log.error('Called `Path.intersectsX(x)` but `x` is not a number') - return this.intersectsAxis(x, 'x') -} - -/** Finds intersections between this path and an Y value */ -Path.prototype.intersectsY = function (y) { - if (typeof y !== 'number') this.log.error('Called `Path.intersectsX(y)` but `y` is not a number') - return this.intersectsAxis(y, 'y') -} - -/** Finds intersections between this path and a X or Y value */ -Path.prototype.intersectsAxis = function (val = false, mode) { - let intersections = [] - let lineStart = mode === 'x' ? new Point(val, -100000) : new Point(-10000, val) - let lineEnd = mode === 'x' ? new Point(val, 100000) : new Point(100000, val) - for (let path of this.divide()) { - if (path.ops[1].type === 'line') { - addIntersectionsToArray( - linesIntersect(path.ops[0].to, path.ops[1].to, lineStart, lineEnd), - intersections - ) - } else if (path.ops[1].type === 'curve') { - addIntersectionsToArray( - lineIntersectsCurve( - lineStart, - lineEnd, - path.ops[0].to, - path.ops[1].cp1, - path.ops[1].cp2, - path.ops[1].to - ), - intersections - ) - } - } - - return intersections -} - -/** Finds intersections between this path and another path */ -Path.prototype.intersects = function (path) { - if (this === path) - this.log.error('You called Path.intersects(path)` but `path` and `this` are the same object') - let intersections = [] - for (let pathA of this.divide()) { - for (let pathB of path.divide()) { - if (pathA.ops[1].type === 'line') { - if (pathB.ops[1].type === 'line') { - addIntersectionsToArray( - linesIntersect(pathA.ops[0].to, pathA.ops[1].to, pathB.ops[0].to, pathB.ops[1].to), - intersections - ) - } else if (pathB.ops[1].type === 'curve') { - addIntersectionsToArray( - lineIntersectsCurve( - pathA.ops[0].to, - pathA.ops[1].to, - pathB.ops[0].to, - pathB.ops[1].cp1, - pathB.ops[1].cp2, - pathB.ops[1].to - ), - intersections - ) - } - } else if (pathA.ops[1].type === 'curve') { - if (pathB.ops[1].type === 'line') { - addIntersectionsToArray( - lineIntersectsCurve( - pathB.ops[0].to, - pathB.ops[1].to, - pathA.ops[0].to, - pathA.ops[1].cp1, - pathA.ops[1].cp2, - pathA.ops[1].to - ), - intersections - ) - } else if (pathB.ops[1].type === 'curve') { - addIntersectionsToArray( - curvesIntersect( - pathA.ops[0].to, - pathA.ops[1].cp1, - pathA.ops[1].cp2, - pathA.ops[1].to, - pathB.ops[0].to, - pathB.ops[1].cp1, - pathB.ops[1].cp2, - pathB.ops[1].to - ), - intersections - ) - } - } - } - } - - return intersections -} - -function addIntersectionsToArray(candidates, intersections) { - if (!candidates) return - if (typeof candidates === 'object') { - if (typeof candidates.x === 'number') intersections.push(candidates) - else { - for (let candidate of candidates) intersections.push(candidate) - } - } -} - -/** Splits path on point, and retuns both halves */ -Path.prototype.split = function (point) { - if (point instanceof Point !== true) - this.log.error('Called `Path.split(point)` but `point` is not a `Point` object') - let divided = this.divide() - let firstHalf = [] - let secondHalf = [] - for (let pi = 0; pi < divided.length; pi++) { - let path = divided[pi] - if (path.ops[1].type === 'line') { - if (path.ops[0].to.sitsRoughlyOn(point)) { - secondHalf.push( - new Path(this.debug).withLog(this.log).move(path.ops[0].to).line(path.ops[1].to) - ) - } else if (path.ops[1].to.sitsRoughlyOn(point)) { - firstHalf.push( - new Path(this.debug).withLog(this.log).move(path.ops[0].to).line(path.ops[1].to) - ) - } else if (pointOnLine(path.ops[0].to, path.ops[1].to, point)) { - firstHalf = divided.slice(0, pi) - firstHalf.push(new Path(this.debug).withLog(this.log).move(path.ops[0].to).line(point)) - pi++ - secondHalf = divided.slice(pi) - secondHalf.unshift(new Path(this.debug).withLog(this.log).move(point).line(path.ops[1].to)) - } - } else if (path.ops[1].type === 'curve') { - if (path.ops[0].to.sitsRoughlyOn(point)) { - secondHalf.push( - new Path(this.debug) - .withLog(this.log) - .move(path.ops[0].to) - .curve(path.ops[1].cp1, path.ops[1].cp2, path.ops[1].to) - ) - } else if (path.ops[1].to.sitsRoughlyOn(point)) { - firstHalf.push( - new Path(this.debug) - .withLog(this.log) - .move(path.ops[0].to) - .curve(path.ops[1].cp1, path.ops[1].cp2, path.ops[1].to) - ) - } else { - let t = pointOnCurve( - path.ops[0].to, - path.ops[1].cp1, - path.ops[1].cp2, - path.ops[1].to, - point - ) - if (t !== false) { - let curve = new Bezier( - { x: path.ops[0].to.x, y: path.ops[0].to.y }, - { x: path.ops[1].cp1.x, y: path.ops[1].cp1.y }, - { x: path.ops[1].cp2.x, y: path.ops[1].cp2.y }, - { x: path.ops[1].to.x, y: path.ops[1].to.y } - ) - let split = curve.split(t) - firstHalf = divided.slice(0, pi) - firstHalf.push( - new Path(this.debug) - .withLog(this.log) - .move(new Point(split.left.points[0].x, split.left.points[0].y)) - .curve( - new Point(split.left.points[1].x, split.left.points[1].y), - new Point(split.left.points[2].x, split.left.points[2].y), - new Point(split.left.points[3].x, split.left.points[3].y) - ) - ) - pi++ - secondHalf = divided.slice(pi) - secondHalf.unshift( - new Path(this.debug) - .withLog(this.log) - .move(new Point(split.right.points[0].x, split.right.points[0].y)) - .curve( - new Point(split.right.points[1].x, split.right.points[1].y), - new Point(split.right.points[2].x, split.right.points[2].y), - new Point(split.right.points[3].x, split.right.points[3].y) - ) - ) - } - } - } - } - if (firstHalf.length > 0) firstHalf = joinPaths(firstHalf, false, this.log) - if (secondHalf.length > 0) secondHalf = joinPaths(secondHalf, false, this.log) - - return [firstHalf, secondHalf] -} - -/** Removes self-intersections (overlap) from the path */ -Path.prototype.trim = function () { - let chunks = this.divide() - for (let i = 0; i < chunks.length; i++) { - let firstCandidate = parseInt(i) + 2 - let lastCandidate = parseInt(chunks.length) - 1 - for (let j = firstCandidate; j < lastCandidate; j++) { - let intersections = chunks[i].intersects(chunks[j]) - if (intersections.length > 0) { - let intersection = intersections.pop() - let trimmedStart = chunks.slice(0, i) - let trimmedEnd = chunks.slice(parseInt(j) + 1) - let glue = new Path(this.debug).withLog(this.log) - let first = true - for (let k of [i, j]) { - let ops = chunks[k].ops - if (ops[1].type === 'line') { - glue.line(intersection) - } else if (ops[1].type === 'curve') { - // handle curve - let curve = new Bezier( - { x: ops[0].to.x, y: ops[0].to.y }, - { x: ops[1].cp1.x, y: ops[1].cp1.y }, - { x: ops[1].cp2.x, y: ops[1].cp2.y }, - { x: ops[1].to.x, y: ops[1].to.y } - ) - let t = pointOnCurve(ops[0].to, ops[1].cp1, ops[1].cp2, ops[1].to, intersection) - let split = curve.split(t) - let side - if (first) side = split.left - else side = split.right - glue.curve( - new Point(side.points[1].x, side.points[1].y), - new Point(side.points[2].x, side.points[2].y), - new Point(side.points[3].x, side.points[3].y) - ) - } - first = false - } - let joint - if (trimmedStart.length > 0) joint = joinPaths(trimmedStart, false, this.log).join(glue) - else joint = glue - if (trimmedEnd.length > 0) joint = joint.join(joinPaths(trimmedEnd, false, this.log)) - - return joint.trim() - } - } - } - - return this -} - -/** Applies a path translate transform */ -Path.prototype.translate = function (x, y) { - if (this.debug) { - if (typeof x !== 'number') - this.log.warning('Called `Path.translate(x, y)` but `x` is not a number') - if (typeof y !== 'number') - this.log.warning('Called `Path.translate(x, y)` but `y` is not a number') - } - let clone = this.clone() - for (let op of clone.ops) { - if (op.type !== 'close') { - op.to = op.to.translate(x, y) - } - if (op.type === 'curve') { - op.cp1 = op.cp1.translate(x, y) - op.cp2 = op.cp2.translate(x, y) - } - } - - return clone -} diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index cf83c78e48a..5bfeed58798 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -1,12 +1,6 @@ import { Attributes } from './attributes.mjs' import pack from 'bin-pack' -import { - addNonEnumProp, - macroName, - sampleStyle, - addPartConfig, - mergeDependencies, -} from './utils.mjs' +import { __addNonEnumProp, __addPartConfig, __macroName } from './utils.mjs' import { Part } from './part.mjs' import { Stack } from './stack.mjs' import { Point } from './point.mjs' @@ -16,28 +10,39 @@ import { Svg } from './svg.mjs' import { Store } from './store.mjs' import { Hooks } from './hooks.mjs' import { version } from '../data.mjs' -import { loadPatternDefaults } from './config.mjs' +import { __loadPatternDefaults } from './config.mjs' +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// + +/** + * Constructor for a Pattern + * + * @constructor + * @param {object} config - The Design config + * @return {object} this - The Pattern instance + */ export function Pattern(config) { // Non-enumerable properties - addNonEnumProp(this, 'plugins', {}) - addNonEnumProp(this, 'parts', [{}]) - addNonEnumProp(this, 'width', 0) - addNonEnumProp(this, 'height', 0) - addNonEnumProp(this, 'autoLayout', { stacks: {} }) - addNonEnumProp(this, 'is', '') - addNonEnumProp(this, 'hooks', new Hooks()) - addNonEnumProp(this, 'Point', Point) - addNonEnumProp(this, 'Path', Path) - addNonEnumProp(this, 'Snippet', Snippet) - addNonEnumProp(this, 'Attributes', Attributes) - addNonEnumProp(this, 'macros', {}) - addNonEnumProp(this, '__parts', {}) - addNonEnumProp(this, '__inject', {}) - addNonEnumProp(this, '__dependencies', {}) - addNonEnumProp(this, '__resolvedDependencies', {}) - addNonEnumProp(this, '__draftOrder', []) - addNonEnumProp(this, '__hide', {}) + __addNonEnumProp(this, 'plugins', {}) + __addNonEnumProp(this, 'parts', [{}]) + __addNonEnumProp(this, 'width', 0) + __addNonEnumProp(this, 'height', 0) + __addNonEnumProp(this, 'autoLayout', { stacks: {} }) + __addNonEnumProp(this, 'is', '') + __addNonEnumProp(this, 'hooks', new Hooks()) + __addNonEnumProp(this, 'Point', Point) + __addNonEnumProp(this, 'Path', Path) + __addNonEnumProp(this, 'Snippet', Snippet) + __addNonEnumProp(this, 'Attributes', Attributes) + __addNonEnumProp(this, 'macros', {}) + __addNonEnumProp(this, '__parts', {}) + __addNonEnumProp(this, '__inject', {}) + __addNonEnumProp(this, '__dependencies', {}) + __addNonEnumProp(this, '__resolvedDependencies', {}) + __addNonEnumProp(this, '__draftOrder', []) + __addNonEnumProp(this, '__hide', {}) // Enumerable properties this.config = config // Design config @@ -47,15 +52,165 @@ export function Pattern(config) { return this } -/* - * We allow late-stage updating of the design config (adding parts for example) - * so we need to do the things we used to do in the contructor at a later stage. - * This methods does that, and resolves the design config + user settings - * Defer some things that used to happen in the constructor to - * and in doing so creating a pattern we can draft +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// + +/** + * FIXME: Allows adding parts to the config at runtime + * + * @param {object} part - The part to add + * @return {object} this - The Pattern instance + */ +Pattern.prototype.addPart = function (part) { + if (typeof part?.draft === 'function') { + if (part.name) { + this.config.parts[part.name] = part + // Add part-level config to config + this.config = __addPartConfig(part, this.config, this.stores[0]) + } else this.stores[0].log.error(`Part must have a name`) + } else this.stores[0].log.error(`Part must have a draft() method`) + + return this +} + +/** + * Drafts this pattern, aka the raison d'etre of FreeSewing + * + * @return {object} this - The Pattern instance + */ +Pattern.prototype.draft = function () { + // Late-stage initialization + this.init() + + // Iterate over the provided sets of settings (typically just one) + for (const set in this.settings) { + // Set store + this.stores[set].log.debug(`📐 Drafting pattern (set ${set})`) + + // Create parts container + this.parts[set] = {} + + // Handle snap for pct options + this.__loadAbsoluteOptionsSet(set) + + this.__runHooks('preDraft') + for (const partName of this.config.draftOrder) { + // Create parts + this.stores[set].log.debug(`📦 Creating part \`${partName}\` (set ${set})`) + this.parts[set][partName] = this.__createPartWithContext(partName, set) + // Handle inject/inheritance + if (typeof this.__inject[partName] === 'string') { + this.stores[set].log.debug( + `Creating part \`${partName}\` from part \`${this.__inject[partName]}\`` + ) + try { + this.parts[set][partName].__inject(this.parts[set][this.__inject[partName]]) + } catch (err) { + this.stores[set].log.error([ + `Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``, + err, + ]) + } + } + if (this.__needs(partName, set)) { + // Draft part + if (typeof this.__parts?.[partName]?.draft === 'function') { + try { + const result = this.__parts[partName].draft(this.parts[set][partName].shorthand()) + if (typeof result === 'undefined') { + this.stores[set].log.error( + `Result of drafting part ${partName} was undefined. Did you forget to return the part?` + ) + } else this.parts[set][partName] = result + } catch (err) { + this.stores[set].log.error([`Unable to draft part \`${partName}\` (set ${set})`, err]) + } + } else this.stores[set].log.error(`Unable to draft pattern. Part.draft() is not callable`) + try { + this.parts[set][partName].render = + this.parts[set][partName].render === false ? false : this.__wants(partName, set) + } catch (err) { + this.stores[set].log.error([ + `Unable to set \`render\` property on part \`${partName}\``, + err, + ]) + } + } else { + this.stores[set].log.debug( + `Part \`${partName}\` is not needed. Skipping draft and setting render to \`false\`` + ) + this.parts[set][partName].render = false + } + } + this.__runHooks('postDraft') + } + + return this +} + +/** Returns props required to render this pattern through + * an external renderer (eg. a React component) + * + * @return {object} this - The Pattern instance + */ +Pattern.prototype.getRenderProps = function () { + // Run pre-render hook + let svg = new Svg(this) + svg.hooks = this.hooks + svg.__runHooks('preRender') + + this.__pack() + // Run post-layout hook + this.__runHooks('postLayout') + let props = { svg } + props.width = this.width + props.height = this.height + props.autoLayout = this.autoLayout + props.settings = this.settings + props.logs = this.stores.map((store) => ({ + debug: store.logs.debug, + info: store.logs.info, + error: store.logs.error, + warning: store.logs.warning, + })) + props.parts = {} + for (let p in this.parts) { + if (this.parts[p].render) { + props.parts[p] = { + paths: this.parts[p].paths, + points: this.parts[p].points, + snippets: this.parts[p].snippets, + attributes: this.parts[p].attributes, + height: this.parts[p].height, + width: this.parts[p].width, + bottomRight: this.parts[p].bottomRight, + topLeft: this.parts[p].topLeft, + store: this.stores[this.parts[p].set], + } + } + } + props.stacks = {} + for (let s in this.stacks) { + if (!this.__isStackHidden(s)) { + props.stacks[s] = this.stacks[s] + } + } + + return props +} + +/** + * Initializes the pattern coniguration and settings + * + * @return {object} this - The Pattern instance */ Pattern.prototype.init = function () { - // Resolve configuration + /* + * We allow late-stage updating of the design config (adding parts for example) + * so we need to do the things we used to do in the contructor at a later stage. + * This methods does that, and resolves the design config + user settings + */ this.__resolveParts() // Resolves parts .__resolveDependencies() // Resolves dependencies .__resolveDraftOrder() // Resolves draft order @@ -77,14 +232,175 @@ Pattern.prototype.init = function () { return this } -Pattern.prototype.__loadConfigData = function () { - if (this.config.data) { - for (const i in this.settings) this.stores[i].set('data', this.config.data) +/** + * Handles pattern sampling + * + * @return {object} this - The Pattern instance + */ +Pattern.prototype.sample = function () { + if (this.settings[0].sample.type === 'option') { + return this.sampleOption(this.settings[0].sample.option) + } else if (this.settings[0].sample.type === 'measurement') { + return this.sampleMeasurement(this.settings[0].sample.measurement) + } else if (this.settings.sample.type === 'models') { + return this.sampleModels(this.settings[0].sample.models, this.settings[0].sample.focus || false) + } +} + +/** + * Handles measurement sampling + * + * @return {object} this - The Pattern instance + */ +Pattern.prototype.sampleMeasurement = function (measurementName) { + this.store.log.debug(`Sampling measurement \`${measurementName}\``) + this.__runHooks('preSample') + this.__applySettings(this.__measurementSets(measurementName)) + this.init() + this.__runHooks('postSample') + + return this.draft() +} + +/** + * Handles models sampling + * + * @return {object} this - The Pattern instance + */ +Pattern.prototype.sampleModels = function (models, focus = false) { + this.store.log.debug(`Sampling models \`${Object.keys(models).join(', ')}\``) + this.__runHooks('preSample') + this.__applySettings(this.__modelSets(models, focus)) + this.init() + this.__runHooks('postSample') + + return this.draft() +} + +/** + * Handles option sampling + * + * @return {object} this - The Pattern instance + */ +Pattern.prototype.sampleOption = function (optionName) { + this.stores[0].log.debug(`Sampling option \`${optionName}\``) + this.__runHooks('preSample') + this.__applySettings(this.__optionSets(optionName)) + this.init() + this.__runHooks('postSample') + + return this.draft() +} + +/** + * Adds a lifecycle hook method to the pattern + * + * @param {string} hook - Name of the lifecycle hook + * @param {function} method - The method to run + * @param {object} data - Any data to pass to the hook method + * @return {object} this - The Pattern instance + */ +Pattern.prototype.on = function (hook, method, data) { + for (const added of this.hooks[hook]) { + // Don't add it twice + if (added.method === method) return this + } + this.hooks[hook].push({ method, data }) + + return this +} + +/** + * Renders the pattern to SVG + * + * @return {string} svg - The rendered SVG + */ +Pattern.prototype.render = function () { + this.svg = new Svg(this) + this.svg.hooks = this.hooks + + return this.__pack().svg.render(this) +} + +/** + * Loads a plugin + * + * @param {object} plugin - The plugin to load + * @param {object} data - Any data to pass to the plugin + * @return {object} this - The Pattern instance + */ +Pattern.prototype.use = function (plugin, data) { + if (this.plugins?.[plugin.name]?.condition && !plugin.condition) { + // Plugin was first loaded conditionally, and is now loaded explicitly + this.stores[0].log.info( + `Plugin \`${plugin.plugin.name} was loaded conditionally earlier, but is now loaded explicitly.` + ) + return this.__loadPlugin(plugin, data) + } + // New plugin + else if (!this.plugins?.[plugin.name]) + return plugin.plugin && plugin.condition + ? this.__useIf(plugin, data) // Conditional plugin + : this.__loadPlugin(plugin, data) // Regular plugin + + this.stores[0].log.info( + `Plugin \`${ + plugin.plugin ? plugin.plugin.name : plugin.name + }\` was requested, but it's already loaded. Skipping.` + ) + + return this +} + +////////////////////////////////////////////// +// 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 (name, part, dep) { + this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name]) + if (typeof this.__parts[dep.name] === 'undefined') { + this.config = __addPartConfig(this.__parts[dep.name], this.config, this.stores[0]) } return this } +/** + * Merges (sets of) settings with the default settings + * + * @private + * @param {Array} sets - An array of settings objects + * @return {object} this - The Pattern instance + */ +Pattern.prototype.__applySettings = function (sets) { + if (!Array.isArray(sets)) throw 'Sets should be an array of settings objects' + if (sets.length === 0) sets.push({}) // Required to load default settings + this.settings = [] + for (const set in sets) { + this.settings.push({ ...__loadPatternDefaults(), ...sets[set] }) + if (set > 0) this.stores.push(new Store()) + } + + return this +} + +/** + * Instantiates a new Part instance and populates it with the pattern context + * + * @private + * @param {string} name - The name of the part + * @param {int} set - The index of the settings set in the list of sets + * @return {Part} part - The instantiated Part + */ Pattern.prototype.__createPartWithContext = function (name, set) { // Context object to add to Part closure const part = new Part() @@ -103,12 +419,19 @@ Pattern.prototype.__createPartWithContext = function (name, set) { } for (const macro in this.macros) { - part[macroName(macro)] = this.macros[macro] + part[__macroName(macro)] = this.macros[macro] } return part } +/** + * Instantiates a new Stack instance and populates it with the pattern context + * + * @private + * @param {string} name - The name of the stack + * @return {Stack} stack - The instantiated Stack + */ Pattern.prototype.__createStackWithContext = function (name) { // Context object to add to Stack closure const stack = new Stack() @@ -122,7 +445,137 @@ Pattern.prototype.__createStackWithContext = function (name) { return stack } -// Merges default for options with user-provided options +/** + * Filter optional measurements out id they are also required measurments + * + * @private + * @return {Pattern} this - The Pattern instance + */ +Pattern.prototype.__filterOptionalMeasurements = function () { + this.config.optionalMeasurements = this.config.optionalMeasurements.filter( + (m) => this.config.measurements.indexOf(m) === -1 + ) + + return this +} + +/** + * Checks whether a part is hidden in the config + * + * @private + * @param {string} partName - Name of the part to check + * @return {bool} hidden - true if the part is hidden, or false if not + */ +Pattern.prototype.__isPartHidden = function (partName) { + if (Array.isArray(this.settings.only)) { + if (this.settings.only.includes(partName)) return false + } + if (this.__parts?.[partName]?.hide) return true + if (this.__parts?.[partName]?.hideAll) return true + + return false +} + +/** + * Checks whether a stack is hidden in the config + * + * @private + * @param {string} stackName - Name of the stack to check + * @return {bool} hidden - true if the part is hidden, or false if not + */ +Pattern.prototype.__isStackHidden = function (stackName) { + if (!this.stacks[stackName]) return true + const parts = this.stacks[stackName].getPartNames() + if (Array.isArray(this.settings.only)) { + for (const partName of parts) { + if (this.settings.only.includes(partName)) return false + } + } + for (const partName of parts) { + if (this.__parts?.[partName]?.hide) return true + if (this.__parts?.[partName]?.hideAll) return true + } + + return false +} + +/** + * Generates an array of settings.options objects for sampling a list option + * + * @private + * @param {string} optionName - Name of the option to sample + * @return {Array} sets - The list of settings objects + */ +Pattern.prototype.__listOptionSets = function (optionName) { + let option = this.config.options[optionName] + const base = this.__setBase() + const sets = [] + let run = 1 + for (const choice of option.list) { + const settings = { + ...base, + options: { + ...base.options, + }, + idPrefix: `sample-${run}`, + partClasses: `sample-${run}`, + } + settings.options[optionName] = choice + sets.push(settings) + run++ + } + + return sets +} + +/** + * Generates an array of settings.absoluteOptions objects for sampling a list option + * + * @private + * @param {string} optionName - Name of the option to sample + * @return {Array} sets - The list of settings objects + */ +Pattern.prototype.__loadAbsoluteOptionsSet = function (set) { + for (const optionName in this.settings[set].options) { + const option = this.config.options[optionName] + if ( + typeof option !== 'undefined' && + typeof option.snap !== 'undefined' && + option.toAbs instanceof Function + ) { + this.settings[set].absoluteOptions[optionName] = this.__snappedPercentageOption( + optionName, + set + ) + this.stores[set].log.debug( + `🧲 Snapped __${optionName}__ to \`${this.settings[set].absoluteOptions[optionName]}\` for set __${set}__` + ) + } + } + + return this +} + +/** + * Loads data from the design config into the store + * + * @private + * @return {Pattern} this - The Pattern instance + */ +Pattern.prototype.__loadConfigData = function () { + if (this.config.data) { + for (const i in this.settings) this.stores[i].set('data', this.config.data) + } + + return this +} + +/** + * Merges defaults for options with user-provided options + * + * @private + * @return {Pattern} this - The Pattern instance + */ Pattern.prototype.__loadOptionDefaults = function () { if (Object.keys(this.config.options).length < 1) return this for (const i in this.settings) { @@ -150,89 +603,40 @@ Pattern.prototype.__loadOptionDefaults = function () { return this } -/* Utility method to get the (initialized) config */ -Pattern.prototype.getConfig = function () { - this.init() - return this.config -} - -/* Utility method to get the (initialized) part list */ -Pattern.prototype.getPartList = function () { - this.init() - return Object.keys(this.config.parts) -} - -Pattern.prototype.__snappedPercentageOption = function (optionName, set) { - const conf = this.config.options[optionName] - const abs = conf.toAbs(this.settings[set].options[optionName], this.settings[set]) - // Handle units-specific config - if (!Array.isArray(conf.snap) && conf.snap.metric && conf.snap.imperial) - conf.snap = conf.snap[this.settings[set].units] - // Simple steps - if (typeof conf.snap === 'number') return Math.ceil(abs / conf.snap) * conf.snap - // List of snaps - if (Array.isArray(conf.snap) && conf.snap.length > 1) { - for (const snap of conf.snap - .sort((a, b) => a - b) - .map((snap, i) => { - const margin = - i < conf.snap.length - 1 - ? (conf.snap[Number(i) + 1] - snap) / 2 // Look forward - : (snap - conf.snap[i - 1]) / 2 // Final snap, look backward - - return { - min: snap - margin, - max: snap + Number(margin), - snap, - } - })) - if (abs <= snap.max && abs >= snap.min) return snap.snap - } - - return abs -} - -Pattern.prototype.runHooks = function (hookName, data = false) { - if (data === false) data = this - let hooks = this.hooks[hookName] - if (hooks.length > 0) { - this.stores[0].log.debug(`Running \`${hookName}\` hooks`) - for (let hook of hooks) { - hook.method(data, hook.data) - } - } -} - -/* - * Allows adding a part at run-time +/** + * Loads a plugin + * + * @private + * @param {object} plugin - The plugin object + * @param {object} data - Any plugin data to load + * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.addPart = function (part) { - if (typeof part?.draft === 'function') { - if (part.name) { - this.config.parts[part.name] = part - // Add part-level config to config - this.config = addPartConfig(part, this.config, this.stores[0]) - } else this.stores[0].log.error(`Part must have a name`) - } else this.stores[0].log.error(`Part must have a draft() method`) +Pattern.prototype.__loadPlugin = function (plugin, data) { + this.plugins[plugin.name] = plugin + if (plugin.hooks) this.__loadPluginHooks(plugin, data) + if (plugin.macros) this.__loadPluginMacros(plugin) + if (plugin.store) this.__loadPluginStoreMethods(plugin) + this.stores[0].log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``) return this } -Pattern.prototype.__loadAbsoluteOptionsSet = function (set) { - for (const optionName in this.settings[set].options) { - const option = this.config.options[optionName] - if ( - typeof option !== 'undefined' && - typeof option.snap !== 'undefined' && - option.toAbs instanceof Function - ) { - this.settings[set].absoluteOptions[optionName] = this.__snappedPercentageOption( - optionName, - set - ) - this.stores[set].log.debug( - `🧲 Snapped __${optionName}__ to \`${this.settings[set].absoluteOptions[optionName]}\` for set __${set}__` - ) +/** + * Loads a plugin's hooks + * + * @private + * @param {object} plugin - The plugin object + * @param {object} data - Any plugin data to load + * @return {Pattern} this - The Pattern instance + */ +Pattern.prototype.__loadPluginHooks = function (plugin, data) { + for (let hook of Object.keys(this.hooks)) { + if (typeof plugin.hooks[hook] === 'function') { + this.on(hook, plugin.hooks[hook], data) + } else if (Array.isArray(plugin.hooks[hook])) { + for (let method of plugin.hooks[hook]) { + this.on(hook, method, data) + } } } @@ -240,171 +644,171 @@ Pattern.prototype.__loadAbsoluteOptionsSet = function (set) { } /** - * The default draft method with pre- and postDraft hooks + * Loads a plugin's macros + * + * @private + * @param {object} plugin - The plugin object + * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.draft = function () { - // Late-stage initialization - this.init() - - // Iterate over the provided sets of settings (typically just one) - for (const set in this.settings) { - // Set store - this.stores[set].log.debug(`📐 Drafting pattern (set ${set})`) - - // Create parts container - this.parts[set] = {} - - // Handle snap for pct options - this.__loadAbsoluteOptionsSet(set) - - this.runHooks('preDraft') - for (const partName of this.config.draftOrder) { - // Create parts - this.stores[set].log.debug(`📦 Creating part \`${partName}\` (set ${set})`) - this.parts[set][partName] = this.__createPartWithContext(partName, set) - // Handle inject/inheritance - if (typeof this.__inject[partName] === 'string') { - this.stores[set].log.debug( - `Creating part \`${partName}\` from part \`${this.__inject[partName]}\`` - ) - try { - this.parts[set][partName].inject(this.parts[set][this.__inject[partName]]) - } catch (err) { - this.stores[set].log.error([ - `Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``, - err, - ]) - } - } - if (this.needs(partName, set)) { - // Draft part - if (typeof this.__parts?.[partName]?.draft === 'function') { - try { - const result = this.__parts[partName].draft(this.parts[set][partName].shorthand()) - if (typeof result === 'undefined') { - this.stores[set].log.error( - `Result of drafting part ${partName} was undefined. Did you forget to return the part?` - ) - } else this.parts[set][partName] = result - } catch (err) { - this.stores[set].log.error([`Unable to draft part \`${partName}\` (set ${set})`, err]) - } - } else this.stores[set].log.error(`Unable to draft pattern. Part.draft() is not callable`) - try { - this.parts[set][partName].render = - this.parts[set][partName].render === false ? false : this.wants(partName, set) - } catch (err) { - this.stores[set].log.error([ - `Unable to set \`render\` property on part \`${partName}\``, - err, - ]) - } - } else { - this.stores[set].log.debug( - `Part \`${partName}\` is not needed. Skipping draft and setting render to \`false\`` - ) - this.parts[set][partName].render = false - } +Pattern.prototype.__loadPluginMacros = function (plugin) { + for (let macro in plugin.macros) { + if (typeof plugin.macros[macro] === 'function') { + this.__macro(macro, plugin.macros[macro]) } - this.runHooks('postDraft') } +} + +/** + * Loads the plugins that are part of the config + * + * @private + * @return {Pattern} this - The Pattern instance + */ +Pattern.prototype.__loadPlugins = function () { + for (const plugin of this.config.plugins) this.use(plugin, plugin.data) return this } /** - * Handles pattern sampling + * Loads a plugin's store methods + * + * @private + * @param {object} plugin - The plugin object + * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.sample = function () { - if (this.settings[0].sample.type === 'option') { - return this.sampleOption(this.settings[0].sample.option) - } else if (this.settings[0].sample.type === 'measurement') { - return this.sampleMeasurement(this.settings[0].sample.measurement) - } else if (this.settings.sample.type === 'models') { - return this.sampleModels(this.settings[0].sample.models, this.settings[0].sample.focus || false) - } +Pattern.prototype.__loadPluginStoreMethods = function (plugin) { + if (Array.isArray(plugin.store)) { + for (const store of this.stores) store.extend(...plugin.store) + } else this.stores[0].log.warning(`Plugin store methods should be an Array`) } -//Pattern.prototype.sampleParts = function () { -// let parts = {} -// this.settings.complete = false -// this.settings.paperless = false -// this.draft() -// for (let i in this.parts) { -// parts[i] = new this.Part() -// parts[i].render = this.parts[i].render -// } -// return parts -//} +/** + * Sets a method for a macro + * + * @private + * @param {string} macro - Name of the macro to run + * @param {function} method - The macro method + * @return {object} this - The Pattern instance + */ +Pattern.prototype.__macro = function (key, method) { + this.macros[key] = method -Pattern.prototype.sampleRun = function (parts, anchors, run, runs, extraClass = false) { - this.draft() - for (let i in this.parts) { - let dx = 0 - let dy = 0 - if (this.parts[i].points.anchor) { - if (typeof anchors[i] === 'undefined') anchors[i] = this.parts[i].points.anchor - else { - if (!anchors[i].sitsOn(this.parts[i].points.anchor)) { - dx = this.parts[i].points.anchor.dx(anchors[i]) - dy = this.parts[i].points.anchor.dy(anchors[i]) - } - } - } - for (let j in this.parts[i].paths) { - parts[i].paths[j + '_' + run] = this.parts[i].paths[j] - .clone() - .attr( - 'style', - extraClass === 'sample-focus' - ? this.settings.sample - ? this.settings.sample.focusStyle || sampleStyle(run, runs) - : sampleStyle(run, runs) - : sampleStyle( - run, - runs, - this.settings.sample ? this.settings.sample.styles || false : false - ) - ) - .attr('data-sample-run', run) - .attr('data-sample-runs', runs) - if (this.parts[i].points.anchor) - parts[i].paths[j + '_' + run] = parts[i].paths[j + '_' + run].translate(dx, dy) - if (extraClass !== false) parts[i].paths[j + '_' + run].attributes.add('class', extraClass) - } - } + return this } -Pattern.prototype.__setBase = function () { - return { - ...this.settings[0], - measurements: { ...(this.settings[0].measurements || {}) }, - options: { ...(this.settings[0].options || {}) }, - } -} - -Pattern.prototype.__listOptionSets = function (optionName) { - let option = this.config.options[optionName] - const base = this.__setBase() +/** + * Generates an array of settings objects for sampling a measurement + * + * @private + * @param {string} measurementName - The name of the measurement to sample + * @return {Array} sets - The list of settings objects + */ +Pattern.prototype.__measurementSets = function (measurementName) { + let val = this.settings[0].measurements[measurementName] + if (val === undefined) + this.stores.log.error( + `Cannot sample measurement \`${measurementName}\` because it's \`undefined\`` + ) + let step = val / 50 + val = val * 0.9 const sets = [] - let run = 1 - for (const choice of option.list) { + const base = this.__setBase() + for (let run = 1; run < 11; run++) { const settings = { ...base, - options: { - ...base.options, + measurements: { + ...base.measurements, }, idPrefix: `sample-${run}`, partClasses: `sample-${run}`, } - settings.options[optionName] = choice + settings.measurements[measurementName] = val sets.push(settings) - run++ + val += step } return sets } +/** + * Generates an array of settings objects for sampling a list of models + * + * @private + * @param {object} models - The models to sample + * @param {string} focus - The ID of the model that should be highlighted + * @return {Array} sets - The list of settings objects + */ +Pattern.prototype.__modelSets = function (models, focus = false) { + const sets = [] + const base = this.__setBase() + let run = 1 + // If there's a focus, do it first so it's at the bottom of the SVG + if (focus) { + sets.push({ + ...base, + measurements: models[focus], + idPrefix: `sample-${run}`, + partClasses: `sample-${run} sample-focus`, + }) + run++ + delete models[focus] + } + for (const measurements of Object.values(models)) { + sets.push({ + ...base, + measurements, + idPrefix: `sample-${run}`, + partClasses: `sample-${run}`, + }) + } + + return sets +} + +/** + * Determines whether a part is needed, depending on the 'only' setting and the configured dependencies + * + * @private + * @param {string} partName - Name of the part + * @param {int} set - The index of the set of settings + * @return {bool} needs - true if the part is needed, or false if not + */ +Pattern.prototype.__needs = function (partName, set = 0) { + // If only is unset, all parts are needed + if ( + typeof this.settings[set].only === 'undefined' || + this.settings[set].only === false || + (Array.isArray(this.settings[set].only) && this.settings[set].only.length === 0) + ) + return true + + // Make only to always be an array + const only = + typeof this.settings[set].only === 'string' + ? [this.settings[set].only] + : this.settings[set].only + + // Walk the only parts, checking each one for a match in its dependencies + for (const part of only) { + if (part === partName) return true + if (this.__resolvedDependencies[part]) { + for (const dependency of this.__resolvedDependencies[part]) { + if (dependency === partName) return true + } + } + } + + return false +} + +/** + * Generates an array of settings objects for sampling an option + * + * @private + * @param {string} optionName - The name of the option to sample + * @return {Array} sets - The list of settings objects + */ Pattern.prototype.__optionSets = function (optionName) { let option = this.config.options[optionName] if (typeof option.list === 'object') return this.__listOptionSets(optionName) @@ -438,221 +842,12 @@ Pattern.prototype.__optionSets = function (optionName) { } /** - * Handles option sampling + * Packs stacks in a 2D space and sets pattern size + * + * @private + * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.sampleOption = function (optionName) { - this.stores[0].log.debug(`Sampling option \`${optionName}\``) - this.runHooks('preSample') - this.__applySettings(this.__optionSets(optionName)) - this.init() - this.runHooks('postSample') - - return this.draft() -} - -//Pattern.prototype.sampleListOption = function (optionName) { -// let parts = this.sampleParts() -// let option = this.config.options[optionName] -// let anchors = {} -// let run = 1 -// let runs = option.list.length -// for (let val of option.list) { -// this.settings.options[optionName] = val -// this.sampleRun(parts, anchors, run, runs) -// run++ -// } -// this.parts = parts -// -// return this -//} - -Pattern.prototype.__measurementSets = function (measurementName) { - let val = this.settings[0].measurements[measurementName] - if (val === undefined) - this.stores.log.error( - `Cannot sample measurement \`${measurementName}\` because it's \`undefined\`` - ) - let step = val / 50 - val = val * 0.9 - const sets = [] - const base = this.__setBase() - for (let run = 1; run < 11; run++) { - const settings = { - ...base, - measurements: { - ...base.measurements, - }, - idPrefix: `sample-${run}`, - partClasses: `sample-${run}`, - } - settings.measurements[measurementName] = val - sets.push(settings) - val += step - } - - return sets -} - -/** - * Handles measurement sampling - */ -Pattern.prototype.sampleMeasurement = function (measurementName) { - this.store.log.debug(`Sampling measurement \`${measurementName}\``) - this.runHooks('preSample') - this.__applySettings(this.__measurementSets(measurementName)) - this.init() - this.runHooks('postSample') - - return this.draft() -} - -Pattern.prototype.__modelSets = function (models, focus = false) { - const sets = [] - const base = this.__setBase() - let run = 1 - // If there's a focus, do it first so it's at the bottom of the SVG - if (focus) { - sets.push({ - ...base, - measurements: models[focus], - idPrefix: `sample-${run}`, - partClasses: `sample-${run} sample-focus`, - }) - run++ - delete models[focus] - } - for (const measurements of Object.values(models)) { - sets.push({ - ...base, - measurements, - idPrefix: `sample-${run}`, - partClasses: `sample-${run}`, - }) - } - - return sets -} - -/** - * Handles models sampling - */ -Pattern.prototype.sampleModels = function (models, focus = false) { - this.store.log.debug(`Sampling models \`${Object.keys(models).join(', ')}\``) - this.runHooks('preSample') - this.__applySettings(this.__modelSets(models, focus)) - this.init() - this.runHooks('postSample') - - return this.draft() -} - -Pattern.prototype.render = function () { - this.svg = new Svg(this) - this.svg.hooks = this.hooks - - return this.pack().svg.render(this) -} - -Pattern.prototype.on = function (hook, method, data) { - for (const added of this.hooks[hook]) { - // Don't add it twice - if (added.method === method) return this - } - this.hooks[hook].push({ method, data }) - - return this -} - -Pattern.prototype.__loadPlugins = function () { - for (const plugin of this.config.plugins) this.use(plugin, plugin.data) - - return this -} - -Pattern.prototype.__loadPlugin = function (plugin, data) { - this.plugins[plugin.name] = plugin - if (plugin.hooks) this.__loadPluginHooks(plugin, data) - if (plugin.macros) this.__loadPluginMacros(plugin) - if (plugin.store) this.__loadPluginStoreMethods(plugin) - this.stores[0].log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``) - - return this -} - -Pattern.prototype.use = function (plugin, data) { - if (this.plugins?.[plugin.name]?.condition && !plugin.condition) { - // Plugin was first loaded conditionally, and is now loaded explicitly - this.stores[0].log.info( - `Plugin \`${plugin.plugin.name} was loaded conditionally earlier, but is now loaded explicitly.` - ) - return this.__loadPlugin(plugin, data) - } - // New plugin - else if (!this.plugins?.[plugin.name]) - return plugin.plugin && plugin.condition - ? this.__useIf(plugin, data) // Conditional plugin - : this.__loadPlugin(plugin, data) // Regular plugin - - this.stores[0].log.info( - `Plugin \`${ - plugin.plugin ? plugin.plugin.name : plugin.name - }\` was requested, but it's already loaded. Skipping.` - ) - - return this -} - -Pattern.prototype.__useIf = function (plugin) { - let load = 0 - for (const set of this.settings) { - if (plugin.condition(set)) load++ - } - if (load > 0) { - this.stores[0].log.info( - `Condition met: Loaded plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` - ) - this.__loadPlugin(plugin.plugin, plugin.data) - } else { - this.stores[0].log.info( - `Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` - ) - } - - return this -} - -Pattern.prototype.__loadPluginHooks = function (plugin, data) { - for (let hook of Object.keys(this.hooks)) { - if (typeof plugin.hooks[hook] === 'function') { - this.on(hook, plugin.hooks[hook], data) - } else if (Array.isArray(plugin.hooks[hook])) { - for (let method of plugin.hooks[hook]) { - this.on(hook, method, data) - } - } - } -} - -Pattern.prototype.__loadPluginMacros = function (plugin) { - for (let macro in plugin.macros) { - if (typeof plugin.macros[macro] === 'function') { - this.macro(macro, plugin.macros[macro]) - } - } -} - -Pattern.prototype.__loadPluginStoreMethods = function (plugin) { - if (Array.isArray(plugin.store)) { - for (const store of this.stores) store.extend(...plugin.store) - } else this.stores[0].log.warning(`Plugin store methods should be an Array`) -} - -Pattern.prototype.macro = function (key, method) { - this.macros[key] = method -} - -/** Packs stacks in a 2D space and sets pattern size */ -Pattern.prototype.pack = function () { +Pattern.prototype.__pack = function () { for (const set in this.settings) { if (this.stores[set].logs.error.length > 0) { this.stores[set].log.warning(`One or more errors occured. Not packing pattern parts`) @@ -676,7 +871,7 @@ Pattern.prototype.pack = function () { for (const [key, stack] of Object.entries(this.stacks)) { // Avoid multiple render calls to cause addition of transforms stack.attributes.remove('transform') - if (!this.isStackHidden(key)) { + if (!this.__isStackHidden(key)) { stack.home() if (this.settings[0].layout === true) bins.push({ id: key, width: stack.width, height: stack.height }) @@ -716,7 +911,42 @@ Pattern.prototype.pack = function () { return this } -/** Determines the order to draft parts in, based on dependencies */ +/** + * 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 + * + * @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) { let sorted = [] let visited = {} @@ -744,41 +974,14 @@ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDepende return this } -/** Recursively solves part dependencies for a part */ -Pattern.prototype.resolveDependency = function (seen, part, graph = this.dependencies, deps = []) { - if (typeof seen[part] === 'undefined') seen[part] = true - if (typeof graph[part] === 'string') graph[part] = [graph[part]] - if (Array.isArray(graph[part])) { - if (graph[part].length === 0) return [] - else { - if (deps.indexOf(graph[part]) === -1) deps.push(...graph[part]) - for (let apart of graph[part]) deps.concat(this.resolveDependency(seen, apart, graph, deps)) - } - } - - return deps -} - -/** Adds a part as a simple dependency **/ -Pattern.prototype.__addDependency = function (name, part, dep) { - this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name]) - if (typeof this.__parts[dep.name] === 'undefined') { - this.config = addPartConfig(this.__parts[dep.name], this.config, this.stores[0]) - } - - return this -} - -/** Filter optional measurements out if they are also required measurements */ -Pattern.prototype.__filterOptionalMeasurements = function () { - this.config.optionalMeasurements = this.config.optionalMeasurements.filter( - (m) => this.config.measurements.indexOf(m) === -1 - ) - - return this -} - -/** Pre-Resolves part dependencies that are passed in 2022 style */ +/** + * Resolves parts and their dependencies + * + * @private + * @param {int} count - The count is used to call itsels recursively + * @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) { if (count === 0) { for (const part of this.config.parts) { @@ -825,13 +1028,19 @@ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) { if (len > count) return this.__resolveParts(len, distance) for (const part of Object.values(this.__parts)) { - this.config = addPartConfig(part, this.config, this.stores[0]) + this.config = __addPartConfig(part, this.config, this.stores[0]) } return this } -/** Resolves part dependencies into a flat array */ +/** + * 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) { @@ -852,7 +1061,7 @@ Pattern.prototype.__resolveDependencies = function (graph = false) { let resolved = {} let seen = {} - for (let part in graph) resolved[part] = this.resolveDependency(seen, part, graph) + 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 @@ -861,44 +1070,114 @@ Pattern.prototype.__resolveDependencies = function (graph = false) { return this } -/** Determines whether a part is needed - * This depends on the 'only' setting and the - * configured dependencies. +/** + * Runs subscriptions to a given lifecycle hook + * + * @private + * @param {string} hookName - Name of the lifecycle hook + * @param {obhect} data - Any data to pass to the hook method + * @return {Pattern} this - The Pattern instance */ -Pattern.prototype.needs = function (partName, set = 0) { - // If only is unset, all parts are needed - if ( - typeof this.settings[set].only === 'undefined' || - this.settings[set].only === false || - (Array.isArray(this.settings[set].only) && this.settings[set].only.length === 0) - ) - return true - - // Make only to always be an array - const only = - typeof this.settings[set].only === 'string' - ? [this.settings[set].only] - : this.settings[set].only - - // Walk the only parts, checking each one for a match in its dependencies - for (const part of only) { - if (part === partName) return true - if (this.__resolvedDependencies[part]) { - for (const dependency of this.__resolvedDependencies[part]) { - if (dependency === partName) return true - } +Pattern.prototype.__runHooks = function (hookName, data = false) { + if (data === false) data = this + let hooks = this.hooks[hookName] + if (hooks.length > 0) { + this.stores[0].log.debug(`Running \`${hookName}\` hooks`) + for (let hook of hooks) { + hook.method(data, hook.data) } } - - return false } -/** Determines whether a part is wanted by the user - * This depends on the 'only' setting +/** + * Returns the base/defaults to generate a set of settings + * + * @private + * @return {object} settings - The settings object */ -Pattern.prototype.wants = function (partName, set = 0) { +Pattern.prototype.__setBase = function () { + return { + ...this.settings[0], + measurements: { ...(this.settings[0].measurements || {}) }, + options: { ...(this.settings[0].options || {}) }, + } +} + +/** + * Returns the absolute value of a snapped percentage option + * + * @private + * @param {string} optionName - The name of the option + * @param {int} set - The index of the set in the list of settings + * @return {float} abs - The absolute value of the snapped option + */ +Pattern.prototype.__snappedPercentageOption = function (optionName, set) { + const conf = this.config.options[optionName] + const abs = conf.toAbs(this.settings[set].options[optionName], this.settings[set]) + // Handle units-specific config + if (!Array.isArray(conf.snap) && conf.snap.metric && conf.snap.imperial) + conf.snap = conf.snap[this.settings[set].units] + // Simple steps + if (typeof conf.snap === 'number') return Math.ceil(abs / conf.snap) * conf.snap + // List of snaps + if (Array.isArray(conf.snap) && conf.snap.length > 1) { + for (const snap of conf.snap + .sort((a, b) => a - b) + .map((snap, i) => { + const margin = + i < conf.snap.length - 1 + ? (conf.snap[Number(i) + 1] - snap) / 2 // Look forward + : (snap - conf.snap[i - 1]) / 2 // Final snap, look backward + + return { + min: snap - margin, + max: snap + Number(margin), + snap, + } + })) + if (abs <= snap.max && abs >= snap.min) return snap.snap + } + + return abs +} + +/** + * Loads a conditional plugin + * + * @private + * @param {object} plugin - The plugin object + * @return {Pattern} this - The Pattern instance + */ +Pattern.prototype.__useIf = function (plugin) { + let load = 0 + for (const set of this.settings) { + if (plugin.condition(set)) load++ + } + if (load > 0) { + this.stores[0].log.info( + `Condition met: Loaded plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` + ) + this.__loadPlugin(plugin.plugin, plugin.data) + } else { + this.stores[0].log.info( + `Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` + ) + } + + return this +} + +/** + * Determines whether a part is wanted, depending on the 'only' setting and the configured dependencies + * + * @private + * @param {string} partName - Name of the part + * @param {int} set - The index of the set of settings + * @return {bool} wants - true if the part is wanted, or false if not + */ +Pattern.prototype.__wants = function (partName, set = 0) { // Hidden parts are not wanted - if (this.isHidden(partName)) return false + if (this.__isPartHidden(partName)) return false else if (typeof this.settings[set].only === 'string') return this.settings[set].only === partName else if (Array.isArray(this.settings[set].only)) { for (const part of this.settings[set].only) { @@ -910,92 +1189,33 @@ Pattern.prototype.wants = function (partName, set = 0) { return true } -/* Checks whether a part is hidden in the config */ -Pattern.prototype.isHidden = function (partName) { - if (Array.isArray(this.settings.only)) { - if (this.settings.only.includes(partName)) return false - } - if (this.__parts?.[partName]?.hide) return true - if (this.__parts?.[partName]?.hideAll) return true - - return false -} - -/* Checks whether (all parts in) a stack is hidden in the config */ -Pattern.prototype.isStackHidden = function (stackName) { - if (!this.stacks[stackName]) return true - const parts = this.stacks[stackName].getPartNames() - if (Array.isArray(this.settings.only)) { - for (const partName of parts) { - if (this.settings.only.includes(partName)) return false - } - } - for (const partName of parts) { - if (this.__parts?.[partName]?.hide) return true - if (this.__parts?.[partName]?.hideAll) return true - } - - return false -} - -/** Returns props required to render this pattern through - * an external renderer (eg. a React component) +////////////////////////////////////////////// +// HELPER METHODS // +////////////////////////////////////////////// +// +/** + * Merges dependencies structure + * + * @private + * @param {array} dep - New dependencies + * @param {array} current - Current dependencies + * @return {array} deps - Merged dependencies */ -Pattern.prototype.getRenderProps = function () { - // Run pre-render hook - let svg = new Svg(this) - svg.hooks = this.hooks - svg.runHooks('preRender') +function mergeDependencies(dep = [], current = []) { + // Current dependencies + const list = [] + if (Array.isArray(current)) list.push(...current) + else if (typeof current === 'string') list.push(current) - this.pack() - // Run post-layout hook - this.runHooks('postLayout') - let props = { svg } - props.width = this.width - props.height = this.height - props.autoLayout = this.autoLayout - props.settings = this.settings - props.logs = this.stores.map((store) => ({ - debug: store.logs.debug, - info: store.logs.info, - error: store.logs.error, - warning: store.logs.warning, - })) - props.parts = {} - for (let p in this.parts) { - if (this.parts[p].render) { - props.parts[p] = { - paths: this.parts[p].paths, - points: this.parts[p].points, - snippets: this.parts[p].snippets, - attributes: this.parts[p].attributes, - height: this.parts[p].height, - width: this.parts[p].width, - bottomRight: this.parts[p].bottomRight, - topLeft: this.parts[p].topLeft, - store: this.stores[this.parts[p].set], - } - } - } - props.stacks = {} - for (let s in this.stacks) { - if (!this.isStackHidden(s)) { - props.stacks[s] = this.stacks[s] - } + 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 props -} - -// Merges settings object with default settings -Pattern.prototype.__applySettings = function (sets) { - if (!Array.isArray(sets)) throw 'Sets should be an array of settings objects' - if (sets.length === 0) sets.push({}) // Required to load default settings - this.settings = [] - for (const set in sets) { - this.settings.push({ ...loadPatternDefaults(), ...sets[set] }) - if (set > 0) this.stores.push(new Store()) - } - - return this + return deps } diff --git a/packages/core/src/point.mjs b/packages/core/src/point.mjs index a958d9a58de..77f64687542 100644 --- a/packages/core/src/point.mjs +++ b/packages/core/src/point.mjs @@ -1,230 +1,319 @@ import { Attributes } from './attributes.mjs' +import { __isCoord, rad2deg, deg2rad } from './utils.mjs' -export function Point(x, y, debug = false) { +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// + +/** + * Constructor for a Point + * + * @constructor + * @param {float} x - X-coordinate of the Point + * @param {float} y - Y-coordinate of the Point + * @return {Point} this - The Point instance + */ +export function Point(x, y) { this.x = x this.y = y this.attributes = new Attributes() - Object.defineProperty(this, 'debug', { value: debug, configurable: true }) } -/** Adds the raise method for a path not created through the proxy **/ -Point.prototype.withRaise = function (raise = false) { - if (raise) Object.defineProperty(this, 'raise', { value: raise }) +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// - return this +/** + * Returns the angle between this Point and that Point + * + * @param {Point} that - The Point instance to calculate the angle with + * @return {float} angle - The angle between this Point and that Point + */ +Point.prototype.angle = function (that) { + let rad = Math.atan2(-1 * this.__check().dy(that.__check()), this.dx(that)) + while (rad < 0) rad += 2 * Math.PI + + return rad2deg(rad) } -/** Debug method to validate point data **/ -Point.prototype.check = function () { - if (typeof this.x !== 'number') this.raise.warning('X value of `Point` is not a number') - if (typeof this.y !== 'number') this.raise.warning('Y value of `Point` is not a number') -} - -/** Radians to degrees */ -Point.prototype.rad2deg = function (radians) { - return radians * 57.29577951308232 -} - -/** Degrees to radians */ -Point.prototype.deg2rad = function (degrees) { - return degrees / 57.29577951308232 -} - -/** Adds an attribute. This is here to make this call chainable in assignment */ +/** + * Chainable way to add an attribute to the Point + * + * @param {string} name - Name of the attribute to add + * @param {string} value - Value of the attribute to add + * @param {bool} overwrite - Whether to overwrite an existing attrubute or not + * @return {object} this - The Point instance + */ Point.prototype.attr = function (name, value, overwrite = false) { - this.check() if (overwrite) this.attributes.set(name, value) else this.attributes.add(name, value) - return this + return this.__check() } -/** Returns the distance between this point and that point */ -Point.prototype.dist = function (that) { - this.check() - that.check() - let dx = this.x - that.x - let dy = this.y - that.y - - return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) -} - -/** Returns slope of a line made by this point and that point */ -Point.prototype.slope = function (that) { - this.check() - that.check() - return (that.y - this.y) / (that.x - this.x) -} - -/** Returns the x-delta between this point and that point */ -Point.prototype.dx = function (that) { - this.check() - that.check() - - return that.x - this.x -} - -/** Returns the y-delta between this point and that point */ -Point.prototype.dy = function (that) { - this.check() - that.check() - - return that.y - this.y -} - -/** Returns the angle between this point and that point */ -Point.prototype.angle = function (that) { - this.check() - that.check() - - let rad = Math.atan2(-1 * this.dy(that), this.dx(that)) - while (rad < 0) rad += 2 * Math.PI - - return this.rad2deg(rad) -} - -/** Rotate this point deg around that point */ -Point.prototype.rotate = function (deg, that) { - if (typeof deg !== 'number') - this.raise.warning('Called `Point.rotate(deg,that)` but `deg` is not a number') - if (that instanceof Point !== true) - this.raise.warning('Called `Point.rotate(deg,that)` but `that` is not a `Point` object') - this.check() - that.check() - let radius = this.dist(that) - let angle = this.angle(that) - let x = that.x + radius * Math.cos(this.deg2rad(angle + deg)) * -1 - let y = that.y + radius * Math.sin(this.deg2rad(angle + deg)) - - return new Point(x, y, this.debug).withRaise(this.raise) -} - -/** returns an identical copy of this point */ -Point.prototype.copy = function () { - this.check() - - return new Point(this.x, this.y, this.debug).withRaise(this.raise) -} - -/** Mirrors this point around X value of that point */ -Point.prototype.flipX = function (that = false) { - this.check() - if (that) { - if (that instanceof Point !== true) - this.raise.warning('Called `Point.rotate(deg,that)` but `that` is not a `Point` object') - that.check() - } - if (that === false || that.x === 0) - return new Point(this.x * -1, this.y, this.debug).withRaise(this.raise) - else return new Point(that.x + this.dx(that), this.y, this.debug).withRaise(this.raise) -} - -/** Mirrors this point around Y value of that point */ -Point.prototype.flipY = function (that = false) { - this.check() - if (that) { - if (that instanceof Point !== true) - this.raise.warning('Called `Point.flipY(that)` but `that` is not a `Point` object') - that.check() - } - if (that === false || that.y === 0) - return new Point(this.x, this.y * -1, this.debug).withRaise(this.raise) - else return new Point(this.x, that.y + this.dy(that), this.debug).withRaise(this.raise) -} - -/** Shifts this point distance in the deg direction */ -Point.prototype.shift = function (deg, distance) { - this.check() - if (typeof deg !== 'number') this.raise.warning('Called `Point.shift` but `deg` is not a number') - if (typeof distance !== 'number') - this.raise.warning('Called `Point.shift` but `distance` is not a number') - let p = this.copy() - p.x += distance - - return p.rotate(deg, this) -} - -/** Shifts this point distance in the direction of that point */ -Point.prototype.shiftTowards = function (that, distance) { - if (typeof distance !== 'number') - this.raise.warning('Called `Point.shiftTowards` but `distance` is not a number') - if (that instanceof Point !== true) - this.raise.warning( - 'Called `Point.shiftTowards(that, distance)` but `that` is not a `Point` object' - ) - this.check() - that.check() - - return this.shift(this.angle(that), distance) -} - -/** Checks whether this has the same coordinates as that */ -Point.prototype.sitsOn = function (that) { - if (that instanceof Point !== true) - this.raise.warning('Called `Point.sitsOn(that)` but `that` is not a `Point` object') - this.check() - that.check() - if (this.x === that.x && this.y === that.y) return true - else return false -} - -/** Checks whether this has roughly the same coordinates as that */ -Point.prototype.sitsRoughlyOn = function (that) { - if (that instanceof Point !== true) - this.raise.warning('Called `Point.sitsRoughlyOn(that)` but `that` is not a `Point` object') - this.check() - that.check() - if (Math.round(this.x) === Math.round(that.x) && Math.round(this.y) === Math.round(that.y)) - return true - else return false -} - -/** Shifts this point fraction of the distance towards that point */ -Point.prototype.shiftFractionTowards = function (that, fraction) { - if (that instanceof Point !== true) - this.raise.warning( - 'Called `Point.shiftFractionTowards(that, fraction)` but `that` is not a `Point` object' - ) - if (typeof fraction !== 'number') - this.raise.warning('Called `Point.shiftFractionTowards` but `fraction` is not a number') - this.check() - that.check() - - return this.shiftTowards(that, this.dist(that) * fraction) -} - -/** Shifts this point distance beyond that point */ -Point.prototype.shiftOutwards = function (that, distance) { - if (that instanceof Point !== true) - this.raise.warning( - 'Called `Point.shiftOutwards(that, distance)` but `that` is not a `Point` object' - ) - if (typeof distance !== 'number') - this.raise.warning( - 'Called `Point.shiftOutwards(that, distance)` but `distance` is not a number' - ) - this.check() - that.check() - - return this.shiftTowards(that, this.dist(that) + distance) -} - -/** Returns a deep copy of this */ +/** + * returns an deel clone of this Point (including coordinates) + * + * @return {Point} clone - The cloned Point instance + */ Point.prototype.clone = function () { - this.check() - const clone = new Point(this.x, this.y, this.debug).withRaise(this.raise) + this.__check() + const clone = new Point(this.x, this.y).__withLog(this.log) clone.attributes = this.attributes.clone() return clone } -/** Applies a translate transform */ +/** + * returns an copy of this Point (coordinates only) + * + * @return {Point} copy - The copied Point instance + */ +Point.prototype.copy = function () { + return new Point(this.__check().x, this.y).__withLog(this.log) +} + +/** + * Returns the distance between this Point and that Point + * + * @param {Point} that - The Point instance to calculate the distance to + * @return {float} distance - The distance between this Point and that Point + */ +Point.prototype.dist = function (that) { + const dx = this.__check().x - that.__check().x + const dy = this.y - that.y + + return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) +} + +/** + * Returns the distance along the X-axis between this Point and that Point (delta X) + * + * @param {Point} that - The Point to which to calcuate the X delta + * @return {float} slote - The X delta + */ +Point.prototype.dx = function (that) { + return that.__check().x - this.__check().x +} + +/** + * Returns the distance along the Y-axis between this Point and that Point (delta Y) + * + * @param {Point} that - The Point to which to calcuate the Y delta + * @return {float} slote - The Y delta + */ +Point.prototype.dy = function (that) { + return that.__check().y - this.__check().y +} + +/** + * Mirrors this Point around the X value of that Point + * + * @param {Point} that - The Point to flip around + * @return {Point} flopped - The new flipped Point instance + */ +Point.prototype.flipX = function (that = false) { + this.__check() + if (that) { + if (that instanceof Point !== true) + this.log.warning('Called `Point.rotate(deg,that)` but `that` is not a `Point` object') + that.__check() + } + if (that === false || that.x === 0) return new Point(this.x * -1, this.y).__withLog(this.log) + else return new Point(that.x + this.dx(that), this.y).__withLog(this.log) +} + +/** + * Mirrors this Point around the Y value of that Point + * + * @param {Point} that - The Point to flip around + * @return {Point} flipped - The new flipped Point instance + */ +Point.prototype.flipY = function (that = false) { + this.__check() + if (that) { + if (that instanceof Point !== true) + this.log.warning('Called `Point.flipY(that)` but `that` is not a `Point` object') + that.__check() + } + if (that === false || that.y === 0) return new Point(this.x, this.y * -1).__withLog(this.log) + else return new Point(this.x, that.y + this.dy(that)).__withLog(this.lo) +} + +/** + * Rotate this Point deg around that Point + * + * @param {float} deg - The degrees to rotate + * @param {Point} that - The Point instance to rotate around + * @return {Point} rotated - The rotated Point instance + */ +Point.prototype.rotate = function (deg, that) { + if (typeof deg !== 'number') + this.log.warning('Called `Point.rotate(deg,that)` but `deg` is not a number') + if (that instanceof Point !== true) + this.log.warning('Called `Point.rotate(deg,that)` but `that` is not a `Point` object') + const radius = this.__check().dist(that.__check()) + const angle = this.angle(that) + const x = that.x + radius * Math.cos(deg2rad(angle + deg)) * -1 + const y = that.y + radius * Math.sin(deg2rad(angle + deg)) + + return new Point(x, y).__withLog(this.log) +} + +/** + * A chainable way to add a circle at a Point + * + * @param {float} radius - The circle radius + * @param {string} className - The CSS classes to apply to the circle + * @return {Point} this - The Point instance + */ +Point.prototype.setCircle = function (radius = false, className = false) { + if (radius) this.attributes.set('data-circle', radius) + if (className) this.attributes.set('data-circle-class', className) + + return this.__check() +} + +/** + * A chainable way to add text to a Point + * + * @param {string} text - The text to add to the Point + * @param {string} className - The CSS classes to apply to the text + * @return {Point} this - The Point instance + */ +Point.prototype.setText = function (text = '', className = false) { + this.attributes.set('data-text', text) + if (className) this.attributes.set('data-text-class', className) + + return this.__check() +} + +/** + * Shifts this Point distance in the deg direction + * + * @param {float} deg - The angle to shift towards + * @param {float} dist - The distance to shift + * @return {Point} shifted - The new shifted Point instance + */ +Point.prototype.shift = function (deg, dist) { + if (typeof deg !== 'number') this.log.warning('Called `Point.shift` but `deg` is not a number') + if (typeof dist !== 'number') + this.log.warning('Called `Point.shift` but `distance` is not a number') + let p = this.__check().copy() + p.x += dist + + return p.rotate(deg, this) +} + +/** + * Shifts this Point a fraction in the direction of that Point + * + * @param {Point} that - The Point to shift towards + * @param {float} fraction - The fraction to shift + * @return {Point} shifted - The new shifted Point instance + */ +Point.prototype.shiftFractionTowards = function (that, fraction) { + if (that instanceof Point !== true) + this.log.warning( + 'Called `Point.shiftFractionTowards(that, fraction)` but `that` is not a `Point` object' + ) + if (typeof fraction !== 'number') + this.log.warning('Called `Point.shiftFractionTowards` but `fraction` is not a number') + + return this.__check().shiftTowards(that.__check(), this.dist(that) * fraction) +} + +/** + * Shifts this Point outwards from that Point + * + * @param {Point} that - The Point to shift outwards from + * @param {float} distance - The distance to shift + * @return {Point} shifted - The new shifted Point instance + */ +Point.prototype.shiftOutwards = function (that, distance) { + if (that instanceof Point !== true) + this.log.warning( + 'Called `Point.shiftOutwards(that, distance)` but `that` is not a `Point` object' + ) + if (typeof distance !== 'number') + this.log.warning('Called `Point.shiftOutwards(that, distance)` but `distance` is not a number') + this.__check() + that.__check() + + return this.__check().shiftTowards(that.__check(), this.dist(that) + distance) +} + +/** + * Shifts this Point distance in the direction of that Point + * + * @param {Point} that - The Point to short towards + * @param {float} dist - The distance to shift + * @return {Point} shifted - The new shifted Point instance + */ +Point.prototype.shiftTowards = function (that, dist) { + if (typeof dist !== 'number') + this.log.warning('Called `Point.shiftTowards` but `distance` is not a number') + if (that instanceof Point !== true) + this.log.warning( + 'Called `Point.shiftTowards(that, distance)` but `that` is not a `Point` object' + ) + + return this.__check().shift(this.angle(that.__check()), dist) +} + +/** + * Checks whether this Point has the same coordinates as that Point + * + * @param {Point} that - The Point to compare coordinates with + * @return {bool} result - True if the Points' coordinates match, false when they do not + */ +Point.prototype.sitsOn = function (that) { + if (that instanceof Point !== true) + this.log.warning('Called `Point.sitsOn(that)` but `that` is not a `Point` object') + if (this.__check().x === that.__check().x && this.y === that.y) return true + else return false +} + +/** + * Checks whether this Point has roughtly the same coordinates as that Point + * + * @param {Point} that - The Point to compare coordinates with + * @return {bool} result - True if the Points' coordinates roughty match, false when they do not + */ +Point.prototype.sitsRoughlyOn = function (that) { + if (that instanceof Point !== true) + this.log.warning('Called `Point.sitsRoughlyOn(that)` but `that` is not a `Point` object') + if ( + Math.round(this.__check().x) === Math.round(that.__check().x) && + Math.round(this.y) === Math.round(that.y) + ) + return true + else return false +} + +/** + * Returns slope of a line made by this Point and that Point + * + * @param {Point} that - The Point that forms the line together with this Point + * @return {float} slote - The slope of the line made by this Point and that Point + */ +Point.prototype.slope = function (that) { + return (that.__check().y - this.__check().y) / (that.x - this.x) +} + +/** + * Returns a Point instance with a translate transform applied + * + * @param {float} x - The X-value of the translate transform + * @param {float} y - The Y-value of the translate transform + * @return {Point} translated - The translated Point instance + */ Point.prototype.translate = function (x, y) { - this.check() + this.__check() if (typeof x !== 'number') - this.raise.warning('Called `Point.translate(x,y)` but `x` is not a number') + this.log.warning('Called `Point.translate(x,y)` but `x` is not a number') if (typeof y !== 'number') - this.raise.warning('Called `Point.translate(x,y)` but `y` is not a number') + this.log.warning('Called `Point.translate(x,y)` but `y` is not a number') const p = this.copy() p.x += x p.y += y @@ -232,18 +321,67 @@ Point.prototype.translate = function (x, y) { return p } -/** Chainable way to set the data-text property (and optional class) */ -Point.prototype.setText = function (text = '', className = false) { - this.attributes.set('data-text', text) - if (className) this.attributes.set('data-text-class', className) +////////////////////////////////////////////// +// PRIVATE METHODS // +////////////////////////////////////////////// + +/** + * Checks the Points coordinates, and raises a warning when they are invalid + * + * @private + * @return {object} this - The Point instance + */ +Point.prototype.__check = function () { + if (typeof this.x !== 'number') this.log.warning('X value of `Point` is not a number') + if (typeof this.y !== 'number') this.log.warning('Y value of `Point` is not a number') return this } -/** Chainable way to set the data-circle property (and optional class) */ -Point.prototype.setCircle = function (radius = false, className = false) { - if (radius) this.attributes.set('data-circle', radius) - if (className) this.attributes.set('data-circle-class', className) +/** + * Adds a logging instance so the Point can log + * + * @private + * @param {object} log - An object holding the logging methods + * @return {object} this - The Point instance + */ +Point.prototype.__withLog = function (log = false) { + if (log) Object.defineProperty(this, 'log', { value: log }) return this } + +////////////////////////////////////////////// +// PUBLIC STATIC METHODS // +////////////////////////////////////////////// + +/** + * Returns a ready-to-proxy that logs when things aren't exactly ok + * + * @private + * @param {object} points - The points object to proxy + * @param {object} log - The logging object + * @return {object} proxy - The object that is ready to be proxied + */ +export function pointsProxy(points, log) { + return { + get: function (...args) { + return Reflect.get(...args) + }, + set: (points, name, value) => { + // Constructor checks + if (value instanceof Point !== true) + log.warning(`\`points.${name}\` was set with a value that is not a \`Point\` object`) + if (value.x == null || !__isCoord(value.x)) + log.warning(`\`points.${name}\` was set with a \`x\` parameter that is not a \`number\``) + if (value.y == null || !__isCoord(value.y)) + log.warning(`\`points.${name}\` was set with a \`y\` parameter that is not a \`number\``) + try { + value.name = name + } catch (err) { + log.warning(`Could not set \`name\` property on \`points.${name}\``) + } + return (points[name] = value) + }, + } +} diff --git a/packages/core/src/snippet.mjs b/packages/core/src/snippet.mjs index 724d547b680..8756f9b121d 100644 --- a/packages/core/src/snippet.mjs +++ b/packages/core/src/snippet.mjs @@ -1,21 +1,38 @@ import { Attributes } from './attributes.mjs' +import { Point } from './point.mjs' -export function Snippet(def, anchor, debug = false) { +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// + +/** + * Constructor for a Snippet + * + * @constructor + * @param {string} def - The id of the snippet in the SVG defs section + * @param {Point} anchor - The Point to anchor this Snippet on + * @return {Snippet} this - The Snippet instance + */ +export function Snippet(def, anchor) { this.def = def this.anchor = anchor this.attributes = new Attributes() - Object.defineProperty(this, 'debug', { value: debug, configurable: true }) return this } -/** Adds the raise method for a snippet not created through the proxy **/ -Snippet.prototype.withRaise = function (raise = false) { - if (raise) Object.defineProperty(this, 'raise', { value: raise }) +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// - return this -} -/** Adds an attribute. This is here to make this call chainable in assignment */ +/** + * Chainable way to add an attribute + * + * @param {string} name - Name of the attribute to add + * @param {string} value - Value of the attribute to add + * @param {bool} overwrite - Whether to overwrite an existing attrubute or not + * @return {Snippet} this - The Snippet instance + */ Snippet.prototype.attr = function (name, value, overwrite = false) { if (overwrite) this.attributes.set(name, value) else this.attributes.add(name, value) @@ -23,10 +40,69 @@ Snippet.prototype.attr = function (name, value, overwrite = false) { return this } -/** Returns a deep copy of this */ +/** + * Returns a deep copy of this snippet + * + * @return {Snippet} clone - A clone of this Snippet instance + */ Snippet.prototype.clone = function () { - let clone = new Snippet(this.def, this.anchor.clone(), this.debug).withRaise(this.raise) + let clone = new Snippet(this.def, this.anchor.clone()).__withLog(this.log) clone.attributes = this.attributes.clone() return clone } + +////////////////////////////////////////////// +// PRIVATE METHODS // +////////////////////////////////////////////// + +/** + * Adds the log method for a snippet not created through the proxy + * + * @private + * @return {Snippet} this - The Snippet instance + */ +Snippet.prototype.__withLog = function (log = false) { + if (log) Object.defineProperty(this, 'log', { value: log }) + + return this +} + +////////////////////////////////////////////// +// PUBLIC STATIC METHODS // +////////////////////////////////////////////// + +/** + * Returns a ready-to-proxy that logs when things aren't exactly ok + * + * @private + * @param {object} snippets - The snippets object to proxy + * @param {object} log - The logging object + * @return {object} proxy - The object that is ready to be proxied + */ +export function snippetsProxy(snippets, log) { + return { + get: function (...args) { + return Reflect.get(...args) + }, + set: (snippets, name, value) => { + // Constructor checks + if (value instanceof Snippet !== true) + log.warning(`\`snippets.${name}\` was set with a value that is not a \`Snippet\` object`) + if (typeof value.def !== 'string') + log.warning( + `\`snippets.${name}\` was set with a \`def\` parameter that is not a \`string\`` + ) + if (value.anchor instanceof Point !== true) + log.warning( + `\`snippets.${name}\` was set with an \`anchor\` parameter that is not a \`Point\`` + ) + try { + value.name = name + } catch (err) { + log.warning(`Could not set \`name\` property on \`snippets.${name}\``) + } + return (snippets[name] = value) + }, + } +} diff --git a/packages/core/src/stack.mjs b/packages/core/src/stack.mjs index 310b55d5230..fd92d2aa25c 100644 --- a/packages/core/src/stack.mjs +++ b/packages/core/src/stack.mjs @@ -4,8 +4,8 @@ import * as utils from './utils.mjs' export function Stack(name = null) { // Non-enumerable properties - utils.addNonEnumProp(this, 'freeId', 0) - utils.addNonEnumProp(this, 'layout', { move: { x: 0, y: 0 } }) + utils.__addNonEnumProp(this, 'freeId', 0) + utils.__addNonEnumProp(this, 'layout', { move: { x: 0, y: 0 } }) // Enumerable properties this.attributes = new Attributes() @@ -36,33 +36,13 @@ Stack.prototype.getPartNames = function () { return [...this.parts].map((p) => p.name) } -/** Homes the stack so that its top left corner is in (0,0) */ -//Stack.prototype.home = function () { -// const parts = this.getPartList() -// if (parts.length < 1) return this -// for (const part of this.getPartList()) { -// part.home() -// } -// -// if (parts.length === 1) { -// this.topLeft = part.topLeft -// this.bottomRigth = part.bottomRight -// this.width = part.width -// this.height = part.height -// -// return this -// } -// -// return this.boundary() -//} - /** Calculates the stack's bounding box and sets it */ Stack.prototype.home = function () { if (this.topLeft) return this // Cached this.topLeft = new Point(Infinity, Infinity) this.bottomRight = new Point(-Infinity, -Infinity) for (const part of this.getPartList()) { - part.boundary() + part.__boundary() if (part.topLeft.x < this.topLeft.x) this.topLeft.x = part.topLeft.x if (part.topLeft.y < this.topLeft.y) this.topLeft.y = part.topLeft.y if (part.bottomRight.x > this.bottomRight.x) this.bottomRight.x = part.bottomRight.x @@ -144,4 +124,24 @@ Stack.prototype.generateTransform = function (transforms) { } } +/** Homes the stack so that its top left corner is in (0,0) */ +//Stack.prototype.home = function () { +// const parts = this.getPartList() +// if (parts.length < 1) return this +// for (const part of this.getPartList()) { +// part.home() +// } +// +// if (parts.length === 1) { +// this.topLeft = part.topLeft +// this.bottomRigth = part.bottomRight +// this.width = part.width +// this.height = part.height +// +// return this +// } +// +// return this.boundary() +//} + export default Stack diff --git a/packages/core/src/store.mjs b/packages/core/src/store.mjs index bf162122916..d39fe002cea 100644 --- a/packages/core/src/store.mjs +++ b/packages/core/src/store.mjs @@ -2,8 +2,20 @@ import set from 'lodash.set' import unset from 'lodash.unset' import get from 'lodash.get' +// Don't allow setting of these top-level keys in the store const avoid = ['set', 'setIfUnset', 'push', 'unset', 'get', 'extend'] +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// + +/** + * Constructor for a Store + * + * @constructor + * @param {Array} methods - Any methods to add to the store + * @return {Store} this - The Store instance + */ export function Store(methods = []) { /* * Default logging methods @@ -40,7 +52,16 @@ export function Store(methods = []) { return this } -/** Extends the store with additional methods */ +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// + +/** + * Extend the store with additional methods + * + * @param {function} method - Method to add to the store (variadic) + * @return {Store} this - The Store instance + */ Store.prototype.extend = function (...methods) { for (const [path, method] of methods) { if (avoid.indexOf(method[0]) !== -1) { @@ -54,29 +75,29 @@ Store.prototype.extend = function (...methods) { return this } -/** Set key at path to value */ -Store.prototype.set = function (path, value) { - if (typeof value === 'undefined') { - this.log.warning(`Store.set(value) on key \`${path}\`, but value is undefined`) +/** + * Retrieve a key from the store + * + * @param {string|array} path - Path to the key + * @param {mixed} dflt - Default method to return if key is undefined + * @return {mixed} value - The value stored under key + */ +Store.prototype.get = function (path, dflt) { + const val = get(this, path, dflt) + if (typeof val === 'undefined') { + this.log.warning(`Store.get(key) on key \`${path}\`, which is undefined`) } - set(this, path, value) - return this + return val } -/** Set key at path to value, but only if it's not currently set */ -Store.prototype.setIfUnset = function (path, value) { - if (typeof value === 'undefined') { - this.log.warning(`Store.setIfUnset(value) on key \`${path}\`, but value is undefined`) - } - if (typeof get(this, path) === 'undefined') { - return set(this, path, value) - } - - return this -} - -/** Adds a value to an array stored under path */ +/** + * Adds a value to an array stored under path + * + * @param {string|array} path - Path to the key + * @param {mixed} values - One or more values to add (variadic) + * @return {Store} this - The Store instance + */ Store.prototype.push = function (path, ...values) { const arr = get(this, path) if (Array.isArray(arr)) { @@ -88,19 +109,49 @@ Store.prototype.push = function (path, ...values) { return this } -/** Remove the key at path */ +/** + * Set key at path to value + * + * @param {string|array} path - Path to the key + * @param {mixed} value - The value to set + * @return {Store} this - The Store instance + */ +Store.prototype.set = function (path, value) { + if (typeof value === 'undefined') { + this.log.warning(`Store.set(value) on key \`${path}\`, but value is undefined`) + } + set(this, path, value) + + return this +} + +/** + * Set key at path to value, but only if it's not currently set + * + * @param {string|array} path - Path to the key + * @param {mixed} value - The value to set + * @return {Store} this - The Store instance + */ +Store.prototype.setIfUnset = function (path, value) { + if (typeof value === 'undefined') { + this.log.warning(`Store.setIfUnset(value) on key \`${path}\`, but value is undefined`) + } + if (typeof get(this, path) === 'undefined') { + return set(this, path, value) + } + + return this +} + +/** + * Remove the key at path + * + * @param {string|array} path - Path to the key + * @param {mixed} value - The value to set + * @return {Store} this - The Store instance + */ Store.prototype.unset = function (path) { unset(this, path) return this } - -/** Retrieve a key */ -Store.prototype.get = function (path, dflt) { - const val = get(this, path, dflt) - if (typeof val === 'undefined') { - this.log.warning(`Store.get(key) on key \`${path}\`, which is undefined`) - } - - return val -} diff --git a/packages/core/src/svg.mjs b/packages/core/src/svg.mjs index 6bd155aa16d..5a57a3590b7 100644 --- a/packages/core/src/svg.mjs +++ b/packages/core/src/svg.mjs @@ -1,16 +1,27 @@ import { Attributes } from './attributes.mjs' -import { addNonEnumProp, round } from './utils.mjs' +import { __addNonEnumProp, round } from './utils.mjs' import { version } from '../data.mjs' +////////////////////////////////////////////// +// CONSTRUCTOR // +////////////////////////////////////////////// + +/** + * Constructor for an Svg + * + * @constructor + * @param {Patern} pattern - The Pattern object to render + * @return {Svg} this - The Path instance + */ export function Svg(pattern) { // Non-enumerable properties - addNonEnumProp(this, 'openGroups', []) - addNonEnumProp(this, 'layout', {}) - addNonEnumProp(this, 'freeId', 0) - addNonEnumProp(this, 'body', '') - addNonEnumProp(this, 'style', '') - addNonEnumProp(this, 'defs', '') - addNonEnumProp(this, 'prefix', '') + __addNonEnumProp(this, 'openGroups', []) + __addNonEnumProp(this, 'layout', {}) + __addNonEnumProp(this, 'freeId', 0) + __addNonEnumProp(this, 'body', '') + __addNonEnumProp(this, 'style', '') + __addNonEnumProp(this, 'defs', '') + __addNonEnumProp(this, 'prefix', '') // Enumerable properties this.pattern = pattern // Needed to expose pattern to hooks @@ -23,18 +34,107 @@ export function Svg(pattern) { this.attributes.add('freesewing', version) } -Svg.prototype.runHooks = function (hookName, data = false) { - if (data === false) data = this - let hooks = this.hooks[hookName] - if (hooks.length > 0) { - for (let hook of hooks) { - hook.method(data, hook.data) +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// + +/** + * Renders a drafted Pattern as SVG + * + * @param {Pattern} pattern - The pattern to render + * @return {string} svg - The rendered SVG output + */ +Svg.prototype.render = function (pattern) { + this.idPrefix = pattern.settings.idPrefix + this.__runHooks('preRender') + pattern.__runHooks('postLayout') + if (!pattern.settings.embed) { + this.attributes.add('width', round(pattern.width) + 'mm') + this.attributes.add('height', round(pattern.height) + 'mm') + } + this.attributes.add('viewBox', `0 0 ${pattern.width} ${pattern.height}`) + this.head = this.__renderHead() + this.tail = this.__renderTail() + this.svg = '' + this.layout = {} // Reset layout + for (let partId in pattern.parts) { + let part = pattern.parts[partId] + if (part.render) { + let partSvg = this.__renderPart(part) + this.layout[partId] = { + svg: partSvg, + transform: part.attributes.getAsArray('transform'), + } + this.svg += this.__openGroup(`${this.idPrefix}part-${partId}`, part.attributes) + this.svg += partSvg + this.svg += this.__closeGroup() } } + this.svg = this.prefix + this.__renderSvgTag() + this.head + this.svg + this.tail + this.__runHooks('postRender') + + return this.svg } -/** Runs insertText hooks */ -Svg.prototype.insertText = function (text) { +////////////////////////////////////////////// +// PRIVATE METHODS // +////////////////////////////////////////////// + +/** + * Returns SVG markup to close a group + * + * @private + * @return {string} svg - The SVG markup to open a group + */ +Svg.prototype.__closeGroup = function () { + this.__outdent() + + return `${this.__nl()}${this.__nl()}` +} + +/** + * Escapes text for SVG output + * + * @private + * @param {string} text - The text to escape + * @return {string} escaped - The escaped text + */ +Svg.prototype.__escapeText = function (text) { + return text.replace(/"/g, '“') +} + +/** + * Returs an unused ID + * + * @private + * @return {numer} id - The next free ID + */ +Svg.prototype.__getId = function () { + this.freeId += 1 + + return '' + this.freeId +} + +/** + * Increases indentation by 1 + * + * @private + * @return {Svg} this - The Svg instance + */ +Svg.prototype.__indent = function () { + this.tabs += 1 + + return this +} + +/** + * Runs the insertText lifecycle hook(s) + * + * @private + * @param {string} text - The text to insert + * @return {Svg} this - The Svg instance + */ +Svg.prototype.__insertText = function (text) { if (this.hooks.insertText.length > 0) { for (let hook of this.hooks.insertText) text = hook.method(this.pattern.settings.locale, text, hook.data) @@ -43,198 +143,188 @@ Svg.prototype.insertText = function (text) { return text } -/** Renders a draft object as SVG */ -Svg.prototype.render = function (pattern) { - this.idPrefix = pattern.settings.idPrefix - this.runHooks('preRender') - pattern.runHooks('postLayout') - if (!pattern.settings.embed) { - this.attributes.add('width', round(pattern.width) + 'mm') - this.attributes.add('height', round(pattern.height) + 'mm') - } - this.attributes.add('viewBox', `0 0 ${pattern.width} ${pattern.height}`) - this.head = this.renderHead() - this.tail = this.renderTail() - this.svg = '' - this.layout = {} // Reset layout - for (let partId in pattern.parts) { - let part = pattern.parts[partId] - if (part.render) { - let partSvg = this.renderPart(part) - this.layout[partId] = { - svg: partSvg, - transform: part.attributes.getAsArray('transform'), - } - this.svg += this.openGroup(`${this.idPrefix}part-${partId}`, part.attributes) - this.svg += partSvg - this.svg += this.closeGroup() - } - } - this.svg = this.prefix + this.renderSvgTag() + this.head + this.svg + this.tail - this.runHooks('postRender') - - return this.svg +/** + * Returns SVG markup for a linebreak + indentation + * + * @private + * @return {string} svg - The Svg markup for a linebreak + indentation + */ +Svg.prototype.__nl = function () { + return '\n' + this.__tab() } -/** Renders SVG head section */ -Svg.prototype.renderHead = function () { - let svg = this.renderStyle() - svg += this.renderScript() - svg += this.renderDefs() - svg += this.openGroup(this.idPrefix + 'container') +/** + * Decreases indentation by 1 + * + * @private + * @return {Svg} this - The Svg instance + */ +Svg.prototype.__outdent = function () { + this.tabs -= 1 + + return this +} + +/** + * Returns SVG markup to open a group + * + * @private + * @param {text} id - The group id + * @param {Attributes} attributes - Any other attributes for the group + * @return {string} svg - The SVG markup to open a group + */ +Svg.prototype.__openGroup = function (id, attributes = false) { + let svg = this.__nl() + this.__nl() + svg += `` + svg += this.__nl() + svg += `' - - return svg +/** + * Returns SVG markup for a circle + * + * @private + * @param {Point} point - The Point instance that holds the circle data + * @return {string} svg - The SVG markup for the circle + */ +Svg.prototype.__renderCircle = function (point) { + return `` } -/** Returns SVG code for the opening SVG tag */ -Svg.prototype.renderSvgTag = function () { - let svg = '' + this.nl() - - return svg -} - -/** Returns SVG code for the style block */ -Svg.prototype.renderStyle = function () { - let svg = '' + this.nl() - return svg -} - -/** Returns SVG code for the script block */ -Svg.prototype.renderScript = function () { - let svg = '' + this.nl() - - return svg -} - -/** Returns SVG code for the defs block */ -Svg.prototype.renderDefs = function () { +/** + * Returns SVG markup for the defs block + * + * @private + * @return {string} svg - The SVG markup for the defs block + */ +Svg.prototype.__renderDefs = function () { let svg = '' - this.indent() - svg += this.nl() + this.defs - this.outdent() - svg += this.nl() + '' + this.nl() + this.__indent() + svg += this.__nl() + this.defs + this.__outdent() + svg += this.__nl() + '' + this.__nl() return svg } -/** Returns SVG code for a Part object */ -Svg.prototype.renderPart = function (part) { - let svg = '' - for (let key in part.paths) { - let path = part.paths[key] - if (path.render) svg += this.renderPath(path) - } - for (let key in part.points) { - if (part.points[key].attributes.get('data-text')) { - svg += this.renderText(part.points[key]) - } - if (part.points[key].attributes.get('data-circle')) { - svg += this.renderCircle(part.points[key]) - } - } - for (let key in part.snippets) { - let snippet = part.snippets[key] - svg += this.renderSnippet(snippet, part) - } +/** + * Returns SVG markup for the head section + * + * @private + * @return {string} svg - The SVG markup for the head section + */ +Svg.prototype.__renderHead = function () { + let svg = this.__renderStyle() + svg += this.__renderScript() + svg += this.__renderDefs() + svg += this.__openGroup(this.idPrefix + 'container') return svg } -/** Returns SVG code for a Path object */ -Svg.prototype.renderPath = function (path) { - if (!path.attributes.get('id')) path.attributes.add('id', this.idPrefix + this.getId()) +/** + * Returns SVG markup for a Path object + * + * @private + * @param {Path} part - The Path instance to render + * @return {string} svg - The SVG markup for the Path object + */ +Svg.prototype.__renderPath = function (path) { + if (!path.attributes.get('id')) path.attributes.add('id', this.idPrefix + this.__getId()) path.attributes.set('d', path.asPathstring()) - return `${this.nl()}${this.renderPathText(path)}` + return `${this.__nl()}${this.__renderPathText(path)}` } -Svg.prototype.renderPathText = function (path) { +/** + * Returns SVG markup for the text on a Path object + * + * @private + * @param {Path} path - The Path instance that holds the text render + * @return {string} svg - The SVG markup for the text on a Path object + */ +Svg.prototype.__renderPathText = function (path) { let text = path.attributes.get('data-text') if (!text) return '' - else this.text = this.insertText(text) + else this.text = this.__insertText(text) let attributes = path.attributes.renderIfPrefixIs('data-text-') // Sadly aligning text along a patch can't be done in CSS only let offset = '' let align = path.attributes.get('data-text-class') if (align && align.indexOf('center') > -1) offset = ' startOffset="50%" ' else if (align && align.indexOf('right') > -1) offset = ' startOffset="100%" ' - let svg = this.nl() + '' - this.indent() + let svg = this.__nl() + '' + this.__indent() svg += `${this.escapeText(this.text)}` - this.outdent() - svg += this.nl() + '' + )}" ${offset}>${this.__escapeText(this.text)}` + this.__outdent() + svg += this.__nl() + '' return svg } -Svg.prototype.renderText = function (point) { - let text = point.attributes.getAsArray('data-text') - if (text !== false) { - let joint = '' - for (let string of text) { - this.text = this.insertText(string) - joint += this.text + ' ' - } - this.text = this.insertText(joint) +/** + * Returns SVG markup for a Part object + * + * @private + * @param {Part} part - The Part instance to render + * @return {string} svg - The SVG markup for the Part object + */ +Svg.prototype.__renderPart = function (part) { + let svg = '' + for (let key in part.paths) { + let path = part.paths[key] + if (path.render) svg += this.__renderPath(path) } - point.attributes.set('data-text-x', round(point.x)) - point.attributes.set('data-text-y', round(point.y)) - let lineHeight = - point.attributes.get('data-text-lineheight') || 6 * (this.pattern.settings.scale || 1) - point.attributes.remove('data-text-lineheight') - let svg = `${this.nl()}` - this.indent() - // Multi-line text? - if (this.text.indexOf('\n') !== -1) { - let lines = this.text.split('\n') - svg += `${lines.shift()}` - for (let line of lines) { - svg += `${line}` + for (let key in part.points) { + if (part.points[key].attributes.get('data-text')) { + svg += this.__renderText(part.points[key]) + } + if (part.points[key].attributes.get('data-circle')) { + svg += this.__renderCircle(part.points[key]) } - } else { - svg += `${this.escapeText(this.text)}` } - this.outdent() - svg += this.nl() + '' + for (let key in part.snippets) { + let snippet = part.snippets[key] + svg += this.__renderSnippet(snippet, part) + } return svg } -Svg.prototype.escapeText = function (text) { - return text.replace(/"/g, '“') +/** + * Returns SVG markup for the script block + * + * @private + * @return {string} svg - The SVG markup for the script block + */ +Svg.prototype.__renderScript = function () { + let svg = '' + this.__nl() + + return svg } -Svg.prototype.renderCircle = function (point) { - return `` -} - -/** Returns SVG code for a snippet */ -Svg.prototype.renderSnippet = function (snippet) { +/** + * Returns SVG markup for a snippet + * + * @private + * @param {Snippet} snippet - The Snippet instance to render + * @return {string} svg - The SVG markup for the snippet + */ +Svg.prototype.__renderSnippet = function (snippet) { let x = round(snippet.anchor.x) let y = round(snippet.anchor.y) let scale = snippet.attributes.get('data-scale') || 1 @@ -248,7 +338,7 @@ Svg.prototype.renderSnippet = function (snippet) { if (rotate) { snippet.attributes.add('transform', `rotate(${rotate}, ${x}, ${y})`) } - let svg = this.nl() + let svg = this.__nl() svg += `` svg += '' @@ -256,34 +346,116 @@ Svg.prototype.renderSnippet = function (snippet) { return svg } -/** Returns SVG code to open a group */ -Svg.prototype.openGroup = function (id, attributes = false) { - let svg = this.nl() + this.nl() - svg += `` - svg += this.nl() - svg += `' + this.__nl() + '' + this.__nl() + return svg +} + +/** + * Returns SVG markup for the opening SVG tag + * + * @private + * @return {string} svg - The SVG markup for the SVG tag + */ +Svg.prototype.__renderSvgTag = function () { + let svg = '' + this.__nl() return svg } -/** Returns SVG code to close a group */ -Svg.prototype.closeGroup = function () { - this.outdent() +/** + * Returns SVG markup for the closing section + * + * @private + * @return {string} svg - The SVG markup for the closing section + */ +Svg.prototype.__renderTail = function () { + let svg = '' + svg += this.__closeGroup() + svg += this.__nl() + '' - return `${this.nl()}${this.nl()}` + return svg } -/** Returns a linebreak + identation */ -Svg.prototype.nl = function () { - return '\n' + this.tab() +/** + * Returns SVG markup for text + * + * @private + * @param {Point} point - The Point instance that holds the text render + * @return {string} svg - The SVG markup for text + */ +Svg.prototype.__renderText = function (point) { + let text = point.attributes.getAsArray('data-text') + if (text !== false) { + let joint = '' + for (let string of text) { + this.text = this.__insertText(string) + joint += this.text + ' ' + } + this.text = this.__insertText(joint) + } + point.attributes.set('data-text-x', round(point.x)) + point.attributes.set('data-text-y', round(point.y)) + let lineHeight = + point.attributes.get('data-text-lineheight') || 6 * (this.pattern.settings.scale || 1) + point.attributes.remove('data-text-lineheight') + let svg = `${this.__nl()}` + this.__indent() + // Multi-line text? + if (this.text.indexOf('\n') !== -1) { + let lines = this.text.split('\n') + svg += `${lines.shift()}` + for (let line of lines) { + svg += `${line}` + } + } else { + svg += `${this.__escapeText(this.text)}` + } + this.__outdent() + svg += this.__nl() + '' + + return svg } -/** Returns indentation */ -Svg.prototype.tab = function () { +/** + * Runs SVG lifecycle hooks + * + * @private + * @param {string} hookName - The lifecycle hook to run + * @param {mixed} data - Any data to pass to the hook method + * @return {string} svg - The SVG markup for the indentation + */ +Svg.prototype.__runHooks = function (hookName, data = false) { + if (data === false) data = this + let hooks = this.hooks[hookName] + if (hooks.length > 0) { + for (let hook of hooks) { + hook.method(data, hook.data) + } + } +} + +/** + * Returns SVG markup for indentation + * + * @private + * @return {string} svg - The SVG markup for the indentation + */ +Svg.prototype.__tab = function () { let space = '' for (let i = 0; i < this.tabs; i++) { space += ' ' @@ -291,20 +463,3 @@ Svg.prototype.tab = function () { return space } - -/** Increases indentation by 1 */ -Svg.prototype.indent = function () { - this.tabs += 1 -} - -/** Decreases indentation by 1 */ -Svg.prototype.outdent = function () { - this.tabs -= 1 -} - -/** Returns an unused ID */ -Svg.prototype.getId = function () { - this.freeId += 1 - - return '' + this.freeId -} diff --git a/packages/core/src/utils.mjs b/packages/core/src/utils.mjs index ec87269e530..75fbba71db5 100644 --- a/packages/core/src/utils.mjs +++ b/packages/core/src/utils.mjs @@ -2,23 +2,88 @@ import { Bezier } from 'bezier-js' import { Path } from './path.mjs' import { Point } from './point.mjs' -export function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1) +////////////////////////////////////////////// +// PUBLIC METHODS // +////////////////////////////////////////////// + +/** + * Find the intersections between an endless line (beam) and a circle + * + * @param {Point} c - The center Point of the circle + * @param {float} r - The radius of the circle + * @param {Point} p1 - First Point on the line + * @param {Point} p2 - Second Point on the line + * @param {string} sort - Controls the sort of the resulting intersections + * @return {Array} intersections - An array with Point objects for the intersections + */ +export function beamIntersectsCircle(c, r, p1, p2, sort = 'x') { + let dx = p2.x - p1.x + let dy = p2.y - p1.y + let A = Math.pow(dx, 2) + Math.pow(dy, 2) + let B = 2 * (dx * (p1.x - c.x) + dy * (p1.y - c.y)) + let C = Math.pow(p1.x - c.x, 2) + Math.pow(p1.y - c.y, 2) - Math.pow(r, 2) + + let det = Math.pow(B, 2) - 4 * A * C + + if (A <= 0.0000001 || det < 0) return false + // No real solutions + else if (det === 0) { + // One solution + let t = (-1 * B) / (2 * A) + let i1 = new Point(p1.x + t * dx, p1.y + t * dy) + return [i1] + } else { + // Two solutions + let t = (-1 * B + Math.sqrt(det)) / (2 * A) + let i1 = new Point(p1.x + t * dx, p1.y + t * dy) + t = (-1 * B - Math.sqrt(det)) / (2 * A) + let i2 = new Point(p1.x + t * dx, p1.y + t * dy) + if ((sort === 'x' && i1.x <= i2.x) || (sort === 'y' && i1.y <= i2.y)) return [i1, i2] + else return [i2, i1] + } } -/** Checks for a valid coordinate value **/ -export function isCoord(value) { - return value === value // NaN does not equal itself - ? typeof value === 'number' - : false +/** + * Finds qhere an endless line intersects with a given X-value + * + * @param {Point} from - First Point on the line + * @param {Point} to - Second Point on the line + * @param {float} x - X-value to check + * @return {Point} intersection - The Point at the intersection + */ +export function beamIntersectsX(from, to, x) { + if (from.x === to.x) return false // Vertical line + let top = new Point(x, -10) + let bottom = new Point(x, 10) + + return beamsIntersect(from, to, top, bottom) } -/** Returns internal hook name for a macro */ -export function macroName(name) { - return `__macro_${name}` +/** + * Finds qhere an endless line intersects with a given Y-value + * + * @param {Point} from - First Point 1 on the line + * @param {Point} to - Second Point on the line + * @param {float} y - Y-value to check + * @return {Point} intersection - The Point at the intersection + */ +export function beamIntersectsY(from, to, y) { + if (from.y === to.y) return false // Horizontal line + let left = new Point(-10, y) + let right = new Point(10, y) + + return beamsIntersect(from, to, left, right) } -/** Find intersection of two (endless) lines */ +/** + * Finds the intersection of two endless lines (beams) + * + * @param {Point} a1 - Point 1 of line A + * @param {Point} a2 - Point 2 of line A + * @param {Point} b1 - Point 1 of line B + * @param {Point} b2 - Point 2 of line B + * @return {Point} intersections - The Point at the intersection + */ export function beamsIntersect(a1, a2, b1, b2) { let slopeA = a1.slope(a2) let slopeB = b1.slope(b2) @@ -46,145 +111,126 @@ export function beamsIntersect(a1, a2, b1, b2) { } } -/** Find intersection of two line segments */ -export function linesIntersect(a1, a2, b1, b2) { - let p = beamsIntersect(a1, a2, b1, b2) - if (!p) return false - let lenA = a1.dist(a2) - let lenB = b1.dist(b2) - let lenC = a1.dist(p) + p.dist(a2) - let lenD = b1.dist(p) + p.dist(b2) - if (Math.round(lenA) == Math.round(lenC) && Math.round(lenB) == Math.round(lenD)) return p - else return false +/** + * Returns the string you pass with with the first character converted to uppercase + * + * @param {string} string - The string to capitalize + * @return {string} capitalized - The capitalized string + */ +export function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1) } -/** Finds out whether a point lies on an endless line */ -export function pointOnBeam(from, to, check, precision = 1e6) { - if (from.sitsOn(check)) return true - if (to.sitsOn(check)) return true - let cross = check.dx(from) * to.dy(from) - check.dy(from) * to.dx(from) - - if (Math.abs(Math.round(cross * precision) / precision) === 0) return true - else return false -} - -/** Finds out whether a point lies on a line segment */ -export function pointOnLine(from, to, check, precision = 1e6) { - if (!pointOnBeam(from, to, check, precision)) return false - let lenA = from.dist(to) - let lenB = from.dist(check) + check.dist(to) - if (Math.round(lenA) == Math.round(lenB)) return true - else return false -} - -/** Finds out whether a point lies on a curve */ -export function pointOnCurve(start, cp1, cp2, end, check) { - if (start.sitsOn(check)) return true - if (end.sitsOn(check)) return true - let curve = new Bezier( - { x: start.x, y: start.y }, - { x: cp1.x, y: cp1.y }, - { x: cp2.x, y: cp2.y }, - { x: end.x, y: end.y } +/** + * Find the intersections between two circles + * + * @param {Point} c1 - The center Point of the first circle + * @param {float} r1 - The radius of the first circle + * @param {Point} c2 - The center Point of the second circle + * @param {float} r2 - The radius of the second circle + * @param {string} sort - Controls the sort of the resulting intersections + * @return {Array} intersections - An array with Point objects for the intersections + */ +export function circlesIntersect(c1, r1, c2, r2, sort = 'x') { + let dx = c1.dx(c2) + let dy = c1.dy(c2) + let dist = c1.dist(c2) + // Check for edge cases + if (dist > parseFloat(r1) + parseFloat(r2)) return false // Circles do not intersect + if (dist < parseFloat(r2) - parseFloat(r1)) return false // One circle is contained in the other + if (dist === 0 && r1 === r2) return false // Two circles are identical + let chorddistance = (Math.pow(r1, 2) - Math.pow(r2, 2) + Math.pow(dist, 2)) / (2 * dist) + let halfchordlength = Math.sqrt(Math.pow(r1, 2) - Math.pow(chorddistance, 2)) + let chordmidpointx = c1.x + (chorddistance * dx) / dist + let chordmidpointy = c1.y + (chorddistance * dy) / dist + let i1 = new Point( + chordmidpointx + (halfchordlength * dy) / dist, + chordmidpointy - (halfchordlength * dx) / dist ) - let intersections = curve.intersects({ - p1: { x: check.x - 1, y: check.y }, - p2: { x: check.x + 1, y: check.y }, - }) - if (intersections.length === 0) { - // Handle edge case of a curve that's a perfect horizontal line - intersections = curve.intersects({ - p1: { x: check.x, y: check.y - 1 }, - p2: { x: check.x, y: check.y + 1 }, - }) - } - - if (intersections.length > 0) return intersections.shift() - else return false -} - -/** Splits a curve on a point */ -export function splitCurve(start, cp1, cp2, end, split) { - let [c1, c2] = new Path().move(start).curve(cp1, cp2, end).split(split) - - return [ - { - start: c1.ops[0].to, - cp1: c1.ops[1].cp1, - cp2: c1.ops[1].cp2, - end: c1.ops[1].to, - }, - { - start: c2.ops[0].to, - cp1: c2.ops[1].cp1, - cp2: c2.ops[1].cp2, - end: c2.ops[1].to, - }, - ] -} - -/** Find where an (endless) line intersects with a certain X-value */ -export function beamIntersectsX(from, to, x) { - if (from.x === to.x) return false // Vertical line - let top = new Point(x, -10) - let bottom = new Point(x, 10) - - return beamsIntersect(from, to, top, bottom) -} - -/** Find where an (endless) line intersects with a certain Y-value */ -export function beamIntersectsY(from, to, y) { - if (from.y === to.y) return false // Horizontal line - let left = new Point(-10, y) - let right = new Point(10, y) - - return beamsIntersect(from, to, left, right) -} - -/** Convert value in mm to cm or imperial units */ -export function units(value, to = 'metric') { - if (to === 'imperial') return round(value / 25.4) + '"' - else return round(value / 10) + 'cm' -} - -/** Find where a curve intersects with line */ -export function lineIntersectsCurve(start, end, from, cp1, cp2, to) { - let intersections = [] - let bz = new Bezier( - { x: from.x, y: from.y }, - { x: cp1.x, y: cp1.y }, - { x: cp2.x, y: cp2.y }, - { x: to.x, y: to.y } + let i2 = new Point( + chordmidpointx - (halfchordlength * dy) / dist, + chordmidpointy + (halfchordlength * dx) / dist ) - let line = { - p1: { x: start.x, y: start.y }, - p2: { x: end.x, y: end.y }, - } - for (let t of bz.intersects(line)) { - let isect = bz.get(t) - intersections.push(new Point(isect.x, isect.y)) - } - if (intersections.length === 0) return false - else if (intersections.length === 1) return intersections[0] - else return intersections + if ((sort === 'x' && i1.x <= i2.x) || (sort === 'y' && i1.y <= i2.y)) return [i1, i2] + else return [i2, i1] } -/** Find where a curve intersects with a given X-value */ +/** + * Finds the edge of a cubic Bezier curve + * + * @param {BezierJs} curve - A BezierJs curve instance + * @param {string} edge - The edge to find: top, bottom, right, or left + * @param {int} steps - The number of steps to divide the curve in while walking it + * @return {Array} intersecions - An Array of Point objects of all intersections + */ +export function curveEdge(curve, edge, steps = 500) { + let x = Infinity + let y = Infinity + let p + if (edge === 'bottom') y = -Infinity + if (edge === 'right') x = -Infinity + for (let i = 0; i < steps; i++) { + p = curve.get(i / steps) + if ( + (edge === 'top' && p.y < y) || + (edge === 'bottom' && p.y > y) || + (edge === 'right' && p.x > x) || + (edge === 'left' && p.x < x) + ) { + x = p.x + y = p.y + } + } + + return new Point(x, y) +} + +/** + * Find where a curve intersections with a given X-value + * + * @param {Point} from - Start Point of the curve + * @param {Point} cp1 - Control Point at the start of the curve + * @param {Point} cp2 - Control Point at the end of the curve + * @param {Point} to - End Point of the curve + * @param {float} x - X-value to check for intersections + * @return {Array} intersecions - An Array of Point objects of all intersections + */ export function curveIntersectsX(from, cp1, cp2, to, x) { let start = new Point(x, -10000) let end = new Point(x, 10000) return lineIntersectsCurve(start, end, from, cp1, cp2, to) } -/** Find where a curve intersects with a given Y-value */ +/** + * Find where a curve intersections with a given Y-value + * + * @param {Point} from - Start Point of the curve + * @param {Point} cp1 - Control Point at the start of the curve + * @param {Point} cp2 - Control Point at the end of the curve + * @param {Point} to - End Point of the curve + * @param {float} y - Y-value to check for intersections + * @return {Array} intersecions - An Array of Point objects of all intersections + */ export function curveIntersectsY(from, cp1, cp2, to, y) { let start = new Point(-10000, y) let end = new Point(10000, y) return lineIntersectsCurve(start, end, from, cp1, cp2, to) } -/** Find where a curve intersects with another curve */ +/** + * Find where a curve intersections with another curve + * + * @param {Point} fromA - Start Point of the first curve + * @param {Point} cp1A - Control Point at the start of the first curve + * @param {Point} cp2A - Control Point at the end of the first curve + * @param {Point} toA - End Point of the first curve + * @param {Point} fromB - Start Point of the second curve + * @param {Point} cp1B - Control Point at the start of the second curve + * @param {Point} cp2B - Control Point at the end of the second curve + * @param {Point} toB - End Point of the fsecond curve + * @return {Array} intersecions - An Array of Point objects of all intersections between the curves + */ export function curvesIntersect(fromA, cp1A, cp2A, toA, fromB, cp1B, cp2B, toB) { let precision = 0.005 // See https://github.com/Pomax/bezierjs/issues/99 let intersections = [] @@ -221,60 +267,83 @@ export function curvesIntersect(fromA, cp1A, cp2A, toA, fromB, cp1B, cp2B, toB) } } -/** Find the intersections between two circles */ -export function circlesIntersect(c1, r1, c2, r2, sort = 'x') { - let dx = c1.dx(c2) - let dy = c1.dy(c2) - let dist = c1.dist(c2) - // Check for edge cases - if (dist > parseFloat(r1) + parseFloat(r2)) return false // Circles do not intersect - if (dist < parseFloat(r2) - parseFloat(r1)) return false // One circle is contained in the other - if (dist === 0 && r1 === r2) return false // Two circles are identical - let chorddistance = (Math.pow(r1, 2) - Math.pow(r2, 2) + Math.pow(dist, 2)) / (2 * dist) - let halfchordlength = Math.sqrt(Math.pow(r1, 2) - Math.pow(chorddistance, 2)) - let chordmidpointx = c1.x + (chorddistance * dx) / dist - let chordmidpointy = c1.y + (chorddistance * dy) / dist - let i1 = new Point( - chordmidpointx + (halfchordlength * dy) / dist, - chordmidpointy - (halfchordlength * dx) / dist - ) - let i2 = new Point( - chordmidpointx - (halfchordlength * dy) / dist, - chordmidpointy + (halfchordlength * dx) / dist - ) - - if ((sort === 'x' && i1.x <= i2.x) || (sort === 'y' && i1.y <= i2.y)) return [i1, i2] - else return [i2, i1] +/** + * Converts degrees to radians + * + * @param {float} degrees - The degrees to convert + * @return {float} radians - The provided degrees in radians + */ +export function deg2rad(degrees) { + return degrees * (Math.PI / 180) } -/** Find the intersections between a beam and a circle */ -export function beamIntersectsCircle(c, r, p1, p2, sort = 'x') { - let dx = p2.x - p1.x - let dy = p2.y - p1.y - let A = Math.pow(dx, 2) + Math.pow(dy, 2) - let B = 2 * (dx * (p1.x - c.x) + dy * (p1.y - c.y)) - let C = Math.pow(p1.x - c.x, 2) + Math.pow(p1.y - c.y, 2) - Math.pow(r, 2) +/** + * Generates the transform attributes needed for a given stack + * + * @param {float} x - The translate value along the X-axis + * @param {float} y - The translate value along the Y-axis + * @param {float} rotate - The rotation + * @param {bool} flipX - Whether or not to flip/mirror along the X-axis + * @param {bool} flipY - Whether or not to flip/mirror along the Y-axis + * @param {Stack} stack - The Stack instance + * @return {string} transform - The SVG transform value + */ +export const generateStackTransform = (x, y, rotate, flipX, flipY, stack) => { + const transforms = [] + let xTotal = x || 0 + let yTotal = y || 0 + let scaleX = 1 + let scaleY = 1 - let det = Math.pow(B, 2) - 4 * A * C + // move the part an additional offset so it ends up in the correct spot after flipping. + // it will scale around the part's 0, 0, which isn't always the top left, so we need to move it over so that 0,0 lines up with topRight + topLeft + if (flipX) { + xTotal += stack.topLeft.x + xTotal += stack.bottomRight.x + // reverse the x scale + scaleX = -1 + } + if (flipY) { + yTotal += stack.topLeft.y + yTotal += stack.bottomRight.y + scaleY = -1 + } - if (A <= 0.0000001 || det < 0) return false - // No real solutions - else if (det === 0) { - // One solution - let t = (-1 * B) / (2 * A) - let i1 = new Point(p1.x + t * dx, p1.y + t * dy) - return [i1] - } else { - // Two solutions - let t = (-1 * B + Math.sqrt(det)) / (2 * A) - let i1 = new Point(p1.x + t * dx, p1.y + t * dy) - t = (-1 * B - Math.sqrt(det)) / (2 * A) - let i2 = new Point(p1.x + t * dx, p1.y + t * dy) - if ((sort === 'x' && i1.x <= i2.x) || (sort === 'y' && i1.y <= i2.y)) return [i1, i2] - else return [i2, i1] + // add the scaling to the transforms + if (scaleX + scaleY < 2) { + transforms.push(`scale(${scaleX} ${scaleY})`) + } + + if (rotate) { + // we can put the center as the rotation origin, so get the center + const center = { + x: stack.topLeft.x + stack.width / 2, + y: stack.topLeft.y + stack.height / 2, + } + + // add the rotation around the center to the transforms + transforms.push(`rotate(${rotate} ${center.x} ${center.y})`) + } + + // put the translation before any other transforms to avoid having to make complex calculations once the matrix has been rotated or scaled + if (xTotal !== 0 || yTotal !== 0) transforms.unshift(`translate(${xTotal} ${yTotal})`) + + return { + transform: transforms.join(' '), + // 'transform-origin': `${center.x} ${center.y}` } } -/** Find the intersections between a line and a circle */ + +/** + * Find the intersections between a line segment and a circle + * + * @param {Point} c - The center Point of the circle + * @param {float} r - The radius of the circle + * @param {Point} p1 - Start Point of the line segment + * @param {Point} p2 - End Point of the line segment + * @param {string} sort - Controls the sort of the resulting intersections + * @return {Array} intersections - An array with Point objects for the intersections + */ export function lineIntersectsCircle(c, r, p1, p2, sort = 'x') { let intersections = beamIntersectsCircle(c, r, p1, p2, sort) if (intersections === false) return false @@ -295,26 +364,190 @@ export function lineIntersectsCircle(c, r, p1, p2, sort = 'x') { } } -export function curveEdge(curve, edge, steps = 500) { - let x = Infinity - let y = Infinity - let p - if (edge === 'bottom') y = -Infinity - if (edge === 'right') x = -Infinity - for (let i = 0; i < steps; i++) { - p = curve.get(i / steps) - if ( - (edge === 'top' && p.y < y) || - (edge === 'bottom' && p.y > y) || - (edge === 'right' && p.x > x) || - (edge === 'left' && p.x < x) - ) { - x = p.x - y = p.y - } +/** + * Finds the intersection of two line segments + * + * @param {Point} a1 - Point 1 of line A + * @param {Point} a2 - Point 2 of line A + * @param {Point} b1 - Point 1 of line B + * @param {Point} b2 - Point 2 of line B + * @return {Point} intersection - The Point at the intersection + */ +export function linesIntersect(a1, a2, b1, b2) { + let p = beamsIntersect(a1, a2, b1, b2) + if (!p) return false + let lenA = a1.dist(a2) + let lenB = b1.dist(b2) + let lenC = a1.dist(p) + p.dist(a2) + let lenD = b1.dist(p) + p.dist(b2) + if (Math.round(lenA) == Math.round(lenC) && Math.round(lenB) == Math.round(lenD)) return p + else return false +} + +/** + * Finds the intersections of a line and a curve + * + * @param {Point} start - Start Point of the line + * @param {Point} end - End Point of the line + * @param {Point} from - Start Point of the curve + * @param {Point} cp1 - Control Point at the start of the curve + * @param {Point} cp2 - Control Point at the end of the curve + * @param {Point} to - End Point of the curve + * @return {Array} intersections - An array of Points at the intersections + */ +export function lineIntersectsCurve(start, end, from, cp1, cp2, to) { + let intersections = [] + let bz = new Bezier( + { x: from.x, y: from.y }, + { x: cp1.x, y: cp1.y }, + { x: cp2.x, y: cp2.y }, + { x: to.x, y: to.y } + ) + let line = { + p1: { x: start.x, y: start.y }, + p2: { x: end.x, y: end.y }, + } + for (let t of bz.intersects(line)) { + let isect = bz.get(t) + intersections.push(new Point(isect.x, isect.y)) } - return new Point(x, y) + if (intersections.length === 0) return false + else if (intersections.length === 1) return intersections[0] + else return intersections +} + +/** + * Helper method to calculate abolute option value based on a measurement + * + * @param {string} measurement - The measurement to base the calculation on + * @return {object} result - An object with the toAbs() and fromAbs() methods + */ +export function pctBasedOn(measurement) { + return { + toAbs: (val, { measurements }) => measurements[measurement] * val, + fromAbs: (val, { measurements }) => + Math.round((10000 * val) / measurements[measurement]) / 10000, + } +} + +/** + * Finds out whether a Point lies on an endless line (beam) + * + * @param {Point} from - First Point on the line + * @param {Point} to - Second Point on the line + * @param {Point} check - Point to check + * @param {float} preciesion - How precise we should check + * @return {bool} result - True of the Point is on the line, false when not + */ +export function pointOnBeam(from, to, check, precision = 1e6) { + if (from.sitsOn(check)) return true + if (to.sitsOn(check)) return true + let cross = check.dx(from) * to.dy(from) - check.dy(from) * to.dx(from) + + if (Math.abs(Math.round(cross * precision) / precision) === 0) return true + else return false +} + +/** + * Finds out whether a Point lies on a (cubic) Bezier curve + * + * @param {Point} from - Start of the curve + * @param {Point} cp1 - Control point at the start of the curve + * @param {Point} cp1 - Control point at the end of the curve + * @param {Point} end - End of the curve + * @param {Point} check - Point to check + * @return {bool} result - True of the Point is on the curve, false when not + */ +export function pointOnCurve(start, cp1, cp2, end, check) { + if (start.sitsOn(check)) return true + if (end.sitsOn(check)) return true + let curve = new Bezier( + { x: start.x, y: start.y }, + { x: cp1.x, y: cp1.y }, + { x: cp2.x, y: cp2.y }, + { x: end.x, y: end.y } + ) + let intersections = curve.intersects({ + p1: { x: check.x - 1, y: check.y }, + p2: { x: check.x + 1, y: check.y }, + }) + if (intersections.length === 0) { + // Handle edge case of a curve that's a perfect horizontal line + intersections = curve.intersects({ + p1: { x: check.x, y: check.y - 1 }, + p2: { x: check.x, y: check.y + 1 }, + }) + } + + if (intersections.length > 0) return intersections.shift() + else return false +} + +/** + * Finds out whether a Point lies on a line segment + * + * @param {Point} from - Start of the line segment + * @param {Point} to - End of the line segment + * @param {Point} check - Point to check + * @param {float} preciesion - How precise we should check + * @return {bool} result - True of the Point is on the line segment, false when not + */ +export function pointOnLine(from, to, check, precision = 1e6) { + if (!pointOnBeam(from, to, check, precision)) return false + let lenA = from.dist(to) + let lenB = from.dist(check) + check.dist(to) + if (Math.round(lenA) == Math.round(lenB)) return true + else return false +} + +/** + * Converts radians to degrees + * + * @param {float} radians - The radiand to convert + * @return {float} degrees - The provided radians in degrees + */ +export function rad2deg(radians) { + return (radians / Math.PI) * 180 +} + +/** + * Rounds a value to 2 digits + * + * @param {float} value - The value to round + * @return {float} rounded - The rounded value + */ +export function round(value) { + return Math.round(value * 1e2) / 1e2 +} + +/** + * Splits curve on a Point + * + * @param {Point} from - Start of the curve + * @param {Point} cp1 - Control point at the start of the curve + * @param {Point} cp1 - Control point at the end of the curve + * @param {Point} end - End of the curve + * @param {Point} split - Point to split the curve on + * @return {Array} halves - An array with the two halves of the Path + */ +export function splitCurve(start, cp1, cp2, end, split) { + let [c1, c2] = new Path().move(start).curve(cp1, cp2, end).split(split) + + return [ + { + start: c1.ops[0].to, + cp1: c1.ops[1].cp1, + cp2: c1.ops[1].cp2, + end: c1.ops[1].to, + }, + { + start: c2.ops[0].to, + cp1: c2.ops[1].cp1, + cp2: c2.ops[1].cp2, + end: c2.ops[1].to, + }, + ] } /** @@ -325,144 +558,61 @@ export function curveEdge(curve, edge, steps = 500) { * When people say '25% stretch' they mean that * 10cm fabric should get stretched to 12.5cm fabric. * In our code, that means we need to scale things by 80%. - * * This method does that calculation. + * + * @param {float} stretch - Strech factor + * @return {float} scale - The scale for the provided stretch factor */ export function stretchToScale(stretch) { return 1 / (1 + parseFloat(stretch)) } -export function round(value) { - return Math.round(value * 1e2) / 1e2 +/** + * Convert value in mm to cm or imperial units + * + * @param {float} value - Value in millimeter + * @param {astring} to - Either 'metric' or 'imperial' + * @return {string} formatted - The value formatted according to the units + */ +export function units(value, to = 'metric') { + if (to === 'imperial') return round(value / 25.4) + '"' + else return round(value / 10) + 'cm' } -export function sampleStyle(run, runs, styles = false) { - return styles && Array.isArray(styles) && styles.length > 0 - ? styles[run % styles.length] - : `stroke: hsl(${(run - 1) * (330 / runs)}, 100%, 35%);` -} +////////////////////////////////////////////// +// PRIVATE METHODS // +////////////////////////////////////////////// -export function deg2rad(degrees) { - return degrees * (Math.PI / 180) -} - -export function rad2deg(radians) { - return (radians / Math.PI) * 180 -} - -// Export bezier-js so plugins can use it -export { Bezier } - -export function pctBasedOn(measurement) { - return { - toAbs: (val, { measurements }) => measurements[measurement] * val, - fromAbs: (val, { measurements }) => - Math.round((10000 * val) / measurements[measurement]) / 10000, - } -} - -/** Generates the transform attributes needed for a given stack */ -export const generateStackTransform = (x, y, rotate, flipX, flipY, part) => { - const transforms = [] - let xTotal = x || 0 - let yTotal = y || 0 - let scaleX = 1 - let scaleY = 1 - - // move the part an additional offset so it ends up in the correct spot after flipping. - // it will scale around the part's 0, 0, which isn't always the top left, so we need to move it over so that 0,0 lines up with topRight + topLeft - if (flipX) { - xTotal += part.topLeft.x - xTotal += part.bottomRight.x - // reverse the x scale - scaleX = -1 - } - if (flipY) { - yTotal += part.topLeft.y - yTotal += part.bottomRight.y - scaleY = -1 - } - - // add the scaling to the transforms - if (scaleX + scaleY < 2) { - transforms.push(`scale(${scaleX} ${scaleY})`) - } - - if (rotate) { - // we can put the center as the rotation origin, so get the center - const center = { - x: part.topLeft.x + part.width / 2, - y: part.topLeft.y + part.height / 2, - } - - // add the rotation around the center to the transforms - transforms.push(`rotate(${rotate} ${center.x} ${center.y})`) - } - - // put the translation before any other transforms to avoid having to make complex calculations once the matrix has been rotated or scaled - if (xTotal !== 0 || yTotal !== 0) transforms.unshift(`translate(${xTotal} ${yTotal})`) - - return { - transform: transforms.join(' '), - // 'transform-origin': `${center.x} ${center.y}` - } -} - -export const mergeDependencies = (dep = [], current = []) => { - // Current dependencies - const list = [] - if (Array.isArray(current)) list.push(...current) - else if (typeof current === 'string') list.push(current) - - 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 -} - -// Decorate an object with a non-enumerable property -export function addNonEnumProp(obj, name, value) { +/** + * Adds a non-enumerable property to an object + * + * @private + * @param {Object} obj - The object to add the property to + * @param {string} name - The name of the property + * @param {mixed} value - The value of the property + * @return {object} obj - The mutated object + */ +export function __addNonEnumProp(obj, name, value) { Object.defineProperty(obj, name, { enumerable: false, configurable: false, writable: true, value, }) + + return obj } -// Add part-level options -const addPartOptions = (part, config, store) => { - if (part.options) { - for (const optionName in part.options) { - if (!config.optionDistance[optionName]) { - config.optionDistance[optionName] = part.distance - config.options[optionName] = part.options[optionName] - store.log.debug(`🔵 __${optionName}__ option loaded from \`${part.name}\``) - } else if (config.optionDistance[optionName] > part.distance) { - config.options[optionName] = part.options[optionName] - store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``) - } - } - } - if (part.from) addPartOptions(part.from, config, store) - if (part.after) { - if (Array.isArray(part.after)) { - for (const dep of part.after) addPartOptions(dep, config, store) - } else addPartOptions(part.after, config, store) - } - - return config -} - -// Add part-level measurements -const addPartMeasurements = (part, config, store, list = false) => { +/** + * Resolves/Adds a part's configured measurements to the global 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 + */ +const __addPartMeasurements = (part, config, store, list = false) => { if (!list) list = config.measurements ? [...config.measurements] : [] if (part.measurements) { for (const m of part.measurements) { @@ -472,11 +622,11 @@ const addPartMeasurements = (part, config, store, list = false) => { } } } - if (part.from) addPartMeasurements(part.from, config, store, list) + if (part.from) __addPartMeasurements(part.from, config, store, list) if (part.after) { if (Array.isArray(part.after)) { - for (const dep of part.after) addPartMeasurements(dep, config, store, list) - } else addPartMeasurements(part.after, config, store, list) + for (const dep of part.after) __addPartMeasurements(dep, config, store, list) + } else __addPartMeasurements(part.after, config, store, list) } // Weed out duplicates @@ -485,8 +635,16 @@ const addPartMeasurements = (part, config, store, list = false) => { return config } -// Add part-level optional measurements -const addPartOptionalMeasurements = (part, config, store, list = false) => { +/** + * 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 {onject} config - The global config + * @param {Store} store - The store, used for logging + * @return {object} config - The mutated global config + */ +const __addPartOptionalMeasurements = (part, config, store, list = false) => { if (!list) list = config.optionalMeasurements ? [...config.optionalMeasurements] : [] if (part.optionalMeasurements) { for (const m of part.optionalMeasurements) { @@ -499,11 +657,11 @@ const addPartOptionalMeasurements = (part, config, store, list = false) => { } } } - if (part.from) addPartOptionalMeasurements(part.from, config, store, list) + if (part.from) __addPartOptionalMeasurements(part.from, config, store, list) if (part.after) { if (Array.isArray(part.after)) { - for (const dep of part.after) addPartOptionalMeasurements(dep, config, store, list) - } else addPartOptionalMeasurements(part.after, config, store, list) + for (const dep of part.after) __addPartOptionalMeasurements(dep, config, store, list) + } else __addPartOptionalMeasurements(part.after, config, store, list) } // Weed out duplicates @@ -512,8 +670,48 @@ const addPartOptionalMeasurements = (part, config, store, list = false) => { return config } -// Add part-level plugins -export const addPartPlugins = (part, config, store) => { +/** + * Resolves/Adds a part's configured options to the global 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 + */ +const __addPartOptions = (part, config, store) => { + if (part.options) { + for (const optionName in part.options) { + if (!config.optionDistance[optionName]) { + config.optionDistance[optionName] = part.distance + config.options[optionName] = part.options[optionName] + store.log.debug(`🔵 __${optionName}__ option loaded from \`${part.name}\``) + } else if (config.optionDistance[optionName] > part.distance) { + config.options[optionName] = part.options[optionName] + store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``) + } + } + } + if (part.from) __addPartOptions(part.from, config, store) + if (part.after) { + if (Array.isArray(part.after)) { + for (const dep of part.after) __addPartOptions(dep, config, store) + } else __addPartOptions(part.after, config, store) + } + + return config +} + +/** + * Resolves/Adds a part's configured plugins to the global 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 + */ +export const __addPartPlugins = (part, config, store) => { const plugins = {} if (!part.plugins) return config for (const plugin of config.plugins) plugins[plugin.name] = plugin @@ -547,15 +745,48 @@ export const addPartPlugins = (part, config, store) => { } } -export const addPartConfig = (part, config, store) => { +/** + * Resolves/Adds a part's configuration to the global 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 + */ +export const __addPartConfig = (part, config, store) => { if (part.resolved) return config // Add parts, using set to keep them unique in the array part.resolved = true config.parts = [...new Set(config.parts).add(part)] - config = addPartOptions(part, config, store) - config = addPartMeasurements(part, config, store) - config = addPartOptionalMeasurements(part, config, store) - config = addPartPlugins(part, config, store) + config = __addPartOptions(part, config, store) + config = __addPartMeasurements(part, config, store) + config = __addPartOptionalMeasurements(part, config, store) + config = __addPartPlugins(part, config, store) return config } + +/** + * Checks whether the paramater passed to it is a valid coordinate (x and y attribute) + * + * @private + * @param {object} value - The object to check + * @return {bool} result - True if it is a valid coordinate, false when not + */ +export function __isCoord(value) { + return value === value // NaN does not equal itself + ? typeof value === 'number' + : false +} + +/** + * Returns the internal hook name for a macro + * + * @private + * @param {string} name - The macro name + * @return {string} macroName - The inernal macroName + */ +export function __macroName(name) { + return `__macro_${name}` +} diff --git a/packages/core/tests/multi.test.mjs b/packages/core/tests/multi.test.mjs index d305eaa5099..5d82998cf02 100644 --- a/packages/core/tests/multi.test.mjs +++ b/packages/core/tests/multi.test.mjs @@ -73,8 +73,5 @@ describe('Multisets', () => { }, ]) pattern.draft() - console.log(pattern) - console.log(pattern.render()) - //pattern.render() }) }) diff --git a/packages/core/tests/part.test.mjs b/packages/core/tests/part.test.mjs index 245a1fe6155..5d3a05af7cf 100644 --- a/packages/core/tests/part.test.mjs +++ b/packages/core/tests/part.test.mjs @@ -20,14 +20,14 @@ describe('Part', () => { expect(typeof dp.context).to.equal('object') }) - it('Should return a function from macroClosure', () => { + it('Should return a function from __macroClosure', () => { const part = new Part() - expect(typeof part.macroClosure()).to.equal('function') + expect(typeof part.__macroClosure()).to.equal('function') }) it('Should not run an unknown macro', () => { const part = new Part() - const macro = part.macroClosure() + const macro = part.__macroClosure() expect(macro('unknown')).to.equal(undefined) }) @@ -62,15 +62,15 @@ describe('Part', () => { expect(part.getId()).to.equal('' + (parseInt(free) + 1)) }) - it('Should return a function from unitsClosure', () => { + it('Should return a function from __unitsClosure', () => { const part = new Part() - expect(typeof part.unitsClosure()).to.equal('function') + expect(typeof part.__unitsClosure()).to.equal('function') }) it('Should convert units', () => { const part = new Part() part.context = { settings: { units: 'metric' } } - const units = part.unitsClosure() + const units = part.__unitsClosure() expect(units(123.456)).to.equal('12.35cm') expect(units(123.456)).to.equal('12.35cm') }) @@ -144,7 +144,7 @@ describe('Part', () => { const design = new Design({ parts: [part] }) const pattern = new design() pattern.draft() - const boundary = pattern.parts[0].test.boundary() + const boundary = pattern.parts[0].test.__boundary() const { topLeft, bottomRight, width, height } = boundary expect(topLeft.x).to.equal(19) expect(topLeft.y).to.equal(76) diff --git a/packages/core/tests/path.test.mjs b/packages/core/tests/path.test.mjs index a703f6161fc..bb97c137bf8 100644 --- a/packages/core/tests/path.test.mjs +++ b/packages/core/tests/path.test.mjs @@ -51,7 +51,7 @@ describe('Path', () => { const pattern = new design() pattern.draft().render() expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(72.63) - expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(26.48) + expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(26.47) }) it('Should offset a curve where cp2 = end', () => { @@ -70,26 +70,6 @@ describe('Path', () => { expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(43.27) }) - /* - it('Should throw error when offsetting line that is no line', () => { - const part = { - name: 'test', - draft: ({ paths, Path, Point }) => { - paths.line = new Path() - .move(new Point(0, 40)) - .line(new Point(0, 40)) - paths.offset = paths.line.offset(10) - return part - } - } - const design = new Design({ parts: [ part ] }) - const pattern = new design() - pattern.draft().render() - console.log(pattern.store.logs) - expect(() => pattern.draft().render()).to.throw() - }) - */ - it('Should return the length of a line', () => { const part = { name: 'test', @@ -121,6 +101,22 @@ describe('Path', () => { expect(round(pattern.parts[0].test.paths.curve.length())).to.equal(145.11) }) + it('Should return the rough length of a curve', () => { + const part = { + name: 'test', + draft: ({ paths, Path, Point, part }) => { + paths.curve = new Path() + .move(new Point(0, 0)) + .curve(new Point(0, 50), new Point(100, 50), new Point(100, 0)) + return part + }, + } + const design = new Design({ parts: [part] }) + const pattern = new design() + pattern.draft().render() + expect(round(pattern.parts[0].test.paths.curve.roughLength())).to.equal(200) + }) + it('Should return the path start point', () => { const part = { name: 'test', @@ -161,7 +157,7 @@ describe('Path', () => { const curve = new Path() .move(new Point(123, 456)) .curve(new Point(0, 40), new Point(123, 34), new Point(230, 4)) - curve.boundary() + curve.__boundary() expect(curve.topLeft.x).to.equal(71.6413460920667) expect(curve.topLeft.y).to.equal(4) expect(curve.bottomRight.x).to.equal(230) @@ -173,7 +169,7 @@ describe('Path', () => { .move(new Point(123, 456)) .curve(new Point(0, 40), new Point(123, 34), new Point(230, 4)) let b = curve.clone() - b.boundary() + b.__boundary() expect(b.topLeft.x).to.equal(71.6413460920667) expect(b.topLeft.y).to.equal(4) b = b.clone() @@ -236,7 +232,7 @@ describe('Path', () => { .curve(new Point(123, 123), new Point(-123, 456), new Point(456, -123)) const a = test.shiftAlong(100) const b = test.reverse().shiftAlong(test.length() - 100) - expect(a.dist(b)).to.below(0.05) + expect(a.dist(b)).to.below(0.2) }) it('Should shift fraction with sufficient precision', () => { @@ -245,7 +241,7 @@ describe('Path', () => { .curve(new Point(123, 123), new Point(-123, 456), new Point(456, -123)) const a = test.shiftFractionAlong(0.5) const b = test.reverse().shiftFractionAlong(0.5) - expect(a.dist(b)).to.below(0.05) + expect(a.dist(b)).to.below(0.2) }) it('Should shift a fraction along a line', () => { @@ -541,10 +537,10 @@ describe('Path', () => { expect(curve.type).to.equal('curve') expect(round(curve.cp1.x)).to.equal(35.08) expect(round(curve.cp1.y)).to.equal(21.64) - expect(round(curve.cp2.x)).to.equal(46.19) - expect(round(curve.cp2.y)).to.equal(-14.69) - expect(round(curve.to.x)).to.equal(72.53) - expect(round(curve.to.y)).to.equal(8.71) + expect(round(curve.cp2.x)).to.equal(46.18) + expect(round(curve.cp2.y)).to.equal(-14.67) + expect(round(curve.to.x)).to.equal(72.51) + expect(round(curve.to.y)).to.equal(8.69) }) it('Should split a path on a line', () => { @@ -683,13 +679,13 @@ describe('Path', () => { it('Should add log methods to a path', () => { const log = () => 'hello' - const p1 = new Path(10, 20).withLog(log) + const p1 = new Path(10, 20).__withLog(log) expect(p1.log()).to.equal('hello') }) it('Should add log methods to a path', () => { const log = () => 'hello' - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) expect(p1.log()).to.equal('hello') }) @@ -707,7 +703,7 @@ describe('Path', () => { it('Should log a warning when moving to a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) expect(invalid).to.equal(false) try { p1.move('a') @@ -720,7 +716,7 @@ describe('Path', () => { it('Should log a warning when drawing a line to a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) expect(invalid).to.equal(false) try { p1.line('a') @@ -733,7 +729,7 @@ describe('Path', () => { it('Should log a warning when drawing a curve to a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) const a = new Point(0, 0) const b = new Point(10, 10) expect(invalid).to.equal(false) @@ -748,7 +744,7 @@ describe('Path', () => { it('Should log a warning when drawing a curve with a Cp1 that is a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) const a = new Point(0, 0) const b = new Point(10, 10) expect(invalid).to.equal(false) @@ -763,7 +759,7 @@ describe('Path', () => { it('Should log a warning when drawing a curve with a Cp1 that is a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) const b = new Point(10, 10) expect(invalid).to.equal(false) try { @@ -777,7 +773,7 @@ describe('Path', () => { it('Should log a warning when drawing a curve with a Cp2 that is a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) const b = new Point(10, 10) expect(invalid).to.equal(false) try { @@ -791,7 +787,7 @@ describe('Path', () => { it('Should log a warning when drawing a _curve with a To that is a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) const b = new Point(10, 10) expect(invalid).to.equal(false) try { @@ -805,7 +801,7 @@ describe('Path', () => { it('Should log a warning when drawing a _curve with a Cp2 that is a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) const b = new Point(10, 10) expect(invalid).to.equal(false) try { @@ -819,7 +815,7 @@ describe('Path', () => { it('Should log a warning when drawing a curve_ with a To that is a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) const b = new Point(10, 10) expect(invalid).to.equal(false) try { @@ -833,7 +829,7 @@ describe('Path', () => { it('Should log a warning when drawing a curve_ with a Cp2 that is a non-point', () => { let invalid = false const log = { warning: () => (invalid = true) } - const p1 = new Path().withLog(log) + const p1 = new Path().__withLog(log) const b = new Point(10, 10) expect(invalid).to.equal(false) try { @@ -867,7 +863,7 @@ describe('Path', () => { const b = new Point(10, 10) const p1 = new Path().move(a).line(b) expect(invalid).to.equal(false) - new Path().withLog(log).noop('test').insop(false, p1) + new Path().__withLog(log).noop('test').insop(false, p1) expect(invalid).to.equal(true) }) @@ -879,7 +875,7 @@ describe('Path', () => { new Path().move(a).line(b) expect(invalid).to.equal(false) try { - new Path().withLog(log).noop('test').insop('test') + new Path().__withLog(log).noop('test').insop('test') } catch (err) { expect('' + err).to.contain("Cannot read properties of undefined (reading 'ops')") } @@ -890,7 +886,7 @@ describe('Path', () => { let invalid = false const log = { warning: () => (invalid = true) } expect(invalid).to.equal(false) - new Path().withLog(log).attr() + new Path().__withLog(log).attr() expect(invalid).to.equal(true) }) @@ -898,7 +894,7 @@ describe('Path', () => { let invalid = false const log = { warning: () => (invalid = true) } expect(invalid).to.equal(false) - new Path().withLog(log).attr('test') + new Path().__withLog(log).attr('test') expect(invalid).to.equal(true) }) @@ -944,7 +940,7 @@ describe('Path', () => { const log = { error: () => (invalid = true) } expect(invalid).to.equal(false) try { - new Path().withLog(log).start() + new Path().__withLog(log).start() } catch (err) { expect('' + err).to.contain("Cannot read properties of undefined (reading 'to')") } @@ -956,7 +952,7 @@ describe('Path', () => { const log = { error: () => (invalid = true) } expect(invalid).to.equal(false) try { - new Path().withLog(log).end() + new Path().__withLog(log).end() } catch (err) { expect('' + err).to.contain("Cannot read properties of undefined (reading 'type')") } @@ -967,7 +963,7 @@ describe('Path', () => { let invalid = false const log = { error: () => (invalid = true) } expect(invalid).to.equal(false) - new Path().withLog(log).move(new Point(0, 0)).line(new Point(10, 10)).shiftAlong() + new Path().__withLog(log).move(new Point(0, 0)).line(new Point(10, 10)).shiftAlong() expect(invalid).to.equal(true) }) diff --git a/packages/core/tests/pattern-draft.test.mjs b/packages/core/tests/pattern-draft.test.mjs index 2005d901098..183c4806067 100644 --- a/packages/core/tests/pattern-draft.test.mjs +++ b/packages/core/tests/pattern-draft.test.mjs @@ -78,9 +78,9 @@ describe('Pattern', () => { only: ['test.partB'], }) pattern.init() - expect(pattern.needs('test.partA')).to.equal(true) - expect(pattern.needs('test.partB')).to.equal(true) - expect(pattern.needs('test.partC')).to.equal(false) + expect(pattern.__needs('test.partA')).to.equal(true) + expect(pattern.__needs('test.partB')).to.equal(true) + expect(pattern.__needs('test.partC')).to.equal(false) }) it('Should check whether a part is wanted', () => { @@ -127,9 +127,9 @@ describe('Pattern', () => { only: ['test.partB'], }) pattern.init() - expect(pattern.wants('test.partA')).to.equal(false) - expect(pattern.wants('test.partB')).to.equal(true) - expect(pattern.wants('test.partC')).to.equal(false) + expect(pattern.__wants('test.partA')).to.equal(false) + expect(pattern.__wants('test.partB')).to.equal(true) + expect(pattern.__wants('test.partC')).to.equal(false) }) /* diff --git a/packages/core/tests/point.test.mjs b/packages/core/tests/point.test.mjs index ffa6e440278..505eee733a0 100644 --- a/packages/core/tests/point.test.mjs +++ b/packages/core/tests/point.test.mjs @@ -227,40 +227,40 @@ describe('Point', () => { expect(p2.y).to.equal(70) }) - it('Should add raise methods to a point', () => { - const raise = () => 'hello' - const p1 = new Point(10, 20).withRaise(raise) - expect(p1.raise()).to.equal('hello') + it('Should add log methods to a point', () => { + const log = () => 'hello' + const p1 = new Point(10, 20).__withLog(log) + expect(p1.log()).to.equal('hello') }) - it('Should raise a warning on invalid point coordinates', () => { + it('Should log a warning on invalid point coordinates', () => { const invalid = { x: false, y: false } - const raiseX = { warning: () => (invalid.x = true) } - const raiseY = { warning: () => (invalid.y = true) } - const p1 = new Point('a', 10).withRaise(raiseX) - const p2 = new Point(20, 'b').withRaise(raiseY) + const logX = { warning: () => (invalid.x = true) } + const logY = { warning: () => (invalid.y = true) } + const p1 = new Point('a', 10).__withLog(logX) + const p2 = new Point(20, 'b').__withLog(logY) expect(invalid.x).to.equal(false) expect(invalid.y).to.equal(false) - p1.check() - p2.check() + p1.__check() + p2.__check() expect(invalid.x).to.equal(true) expect(invalid.y).to.equal(true) }) - it('Should raise a warning if rotation is not a number', () => { + it('Should log a warning if rotation is not a number', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) - const p2 = new Point(20, 20).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) + const p2 = new Point(20, 20).__withLog(log) expect(invalid).to.equal(false) p1.rotate('a', p2) expect(invalid).to.equal(true) }) - it('Should raise a warning if rotating around what is not a point', () => { + it('Should log a warning if rotating around what is not a point', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.rotate(45, 'a') @@ -270,10 +270,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it("Should raise a warning when flipX'ing around what is not a point", () => { + it("Should log a warning when flipX'ing around what is not a point", () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.flipX('a') @@ -283,10 +283,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it("Should raise a warning when flipY'ing around what is not a point", () => { + it("Should log a warning when flipY'ing around what is not a point", () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.flipY('a') @@ -296,10 +296,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when shifting with a distance that is not a number', () => { + it('Should log a warning when shifting with a distance that is not a number', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.shift(0, 'a') @@ -309,10 +309,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when shifting with an angle that is not a number', () => { + it('Should log a warning when shifting with an angle that is not a number', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.shift('a', 12) @@ -322,11 +322,11 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when shifting towards with a distance that is not a number', () => { + it('Should log a warning when shifting towards with a distance that is not a number', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) - const p2 = new Point(20, 20).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) + const p2 = new Point(20, 20).__withLog(log) expect(invalid).to.equal(false) try { p1.shiftTowards(p2, 'a') @@ -336,10 +336,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when shifting towards with a target that is not a point', () => { + it('Should log a warning when shifting towards with a target that is not a point', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.shiftTowards('a', 10) @@ -349,11 +349,11 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when shifting fraction towards with a distance that is not a number', () => { + it('Should log a warning when shifting fraction towards with a distance that is not a number', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) - const p2 = new Point(20, 20).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) + const p2 = new Point(20, 20).__withLog(log) expect(invalid).to.equal(false) try { p1.shiftFractionTowards(p2, 'a') @@ -363,10 +363,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when shifting a fraction towards with a target that is not a point', () => { + it('Should log a warning when shifting a fraction towards with a target that is not a point', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.shiftFractionTowards('a', 0.1) @@ -376,11 +376,11 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when shifting outowards with a distance that is not a number', () => { + it('Should log a warning when shifting outowards with a distance that is not a number', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) - const p2 = new Point(20, 20).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) + const p2 = new Point(20, 20).__withLog(log) expect(invalid).to.equal(false) try { p1.shiftOutwards(p2, 'a') @@ -390,10 +390,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when shifting a outowards with a target that is not a point', () => { + it('Should log a warning when shifting a outowards with a target that is not a point', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.shiftOutwards('a', 0.1) @@ -403,10 +403,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when translating with an X-delta that is not a number', () => { + it('Should log a warning when translating with an X-delta that is not a number', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.translate('a', 10) @@ -416,10 +416,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when translating with an Y-delta that is not a number', () => { + it('Should log a warning when translating with an Y-delta that is not a number', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.translate(10, 'a') @@ -429,10 +429,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when sitsOn receives a non-point', () => { + it('Should log a warning when sitsOn receives a non-point', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.sitsOn('a') @@ -442,10 +442,10 @@ describe('Point', () => { expect(invalid).to.equal(true) }) - it('Should raise a warning when sitsRoughlyOn receives a non-point', () => { + it('Should log a warning when sitsRoughlyOn receives a non-point', () => { let invalid = false - const raise = { warning: () => (invalid = true) } - const p1 = new Point(10, 10).withRaise(raise) + const log = { warning: () => (invalid = true) } + const p1 = new Point(10, 10).__withLog(log) expect(invalid).to.equal(false) try { p1.sitsRoughlyOn('a') diff --git a/packages/core/tests/utils.test.mjs b/packages/core/tests/utils.test.mjs index 6a39614b889..a80ce914b1a 100644 --- a/packages/core/tests/utils.test.mjs +++ b/packages/core/tests/utils.test.mjs @@ -1,7 +1,6 @@ import chai from 'chai' import { Point, - isCoord, capitalize, beamsIntersect, linesIntersect, @@ -21,20 +20,14 @@ import { lineIntersectsCircle, stretchToScale, round, - sampleStyle, deg2rad, rad2deg, pctBasedOn, - macroName, } from '../src/index.mjs' const { expect } = chai describe('Utils', () => { - it('Should return the correct macro name', () => { - expect(macroName('test')).to.equal('__macro_test') - }) - it('Should find the intersection of two endless line segments', () => { let a = new Point(10, 20) let b = new Point(20, 24) @@ -463,34 +456,6 @@ describe('Utils', () => { expect(round(i.y)).to.equal(400) }) - it('Should check for valid coordinate', () => { - expect(isCoord(23423.23)).to.equal(true) - expect(isCoord(0)).to.equal(true) - expect(isCoord()).to.equal(false) - expect(isCoord(null)).to.equal(false) - expect(isCoord('hi')).to.equal(false) - expect(isCoord(NaN)).to.equal(false) - }) - - it('Should return the correct sample style', () => { - expect(sampleStyle(0, 5)).to.equal('stroke: hsl(-66, 100%, 35%);') - expect(sampleStyle(1, 5)).to.equal('stroke: hsl(0, 100%, 35%);') - expect(sampleStyle(2, 5)).to.equal('stroke: hsl(66, 100%, 35%);') - expect(sampleStyle(3, 5)).to.equal('stroke: hsl(132, 100%, 35%);') - expect(sampleStyle(4, 5)).to.equal('stroke: hsl(198, 100%, 35%);') - }) - - it('Should return the correct sample styles', () => { - const styles = [ - 'stroke: red;', - 'stroke: blue;', - 'stroke: green;', - 'stroke: pink;', - 'stroke: orange;', - ] - for (let i = 0; i < 5; i++) expect(sampleStyle(i, 5, styles)).to.equal(styles[i]) - }) - it('Should convert degrees to radians', () => { expect(deg2rad(0)).to.equal(0) expect(round(deg2rad(69))).to.equal(1.2) diff --git a/yarn.lock b/yarn.lock index 89a50ceeded..89d3e48bbd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -378,6 +378,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539" integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg== +"@babel/parser@^7.9.4": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.1.tgz#6f6d6c2e621aad19a92544cc217ed13f1aac5b4c" + integrity sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -3474,6 +3479,19 @@ dependencies: "@types/node" "*" +"@types/linkify-it@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" + integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== + +"@types/markdown-it@^12.2.3": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" + "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -3481,7 +3499,7 @@ dependencies: "@types/unist" "*" -"@types/mdurl@^1.0.0": +"@types/mdurl@*", "@types/mdurl@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== @@ -4908,7 +4926,7 @@ bl@^5.0.0: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@^3.5.5: +bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -5443,6 +5461,13 @@ capture-stack-trace@^1.0.0: resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== +catharsis@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" + integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== + dependencies: + lodash "^4.17.15" + cbor@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/cbor/-/cbor-8.1.0.tgz#cfc56437e770b73417a2ecbfc9caf6b771af60d5" @@ -7495,6 +7520,11 @@ entities@^4.2.0, entities@^4.3.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.1.tgz#c34062a94c865c322f9d67b4384e4169bcede6a4" integrity sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg== +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -9519,7 +9549,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -11191,6 +11221,34 @@ js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +js2xmlparser@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" + integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== + dependencies: + xmlcreate "^2.0.4" + +jsdoc@^3.6.11: + version "3.6.11" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.6.11.tgz#8bbb5747e6f579f141a5238cbad4e95e004458ce" + integrity sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg== + dependencies: + "@babel/parser" "^7.9.4" + "@types/markdown-it" "^12.2.3" + bluebird "^3.7.2" + catharsis "^0.9.0" + escape-string-regexp "^2.0.0" + js2xmlparser "^4.0.2" + klaw "^3.0.0" + markdown-it "^12.3.2" + markdown-it-anchor "^8.4.1" + marked "^4.0.10" + mkdirp "^1.0.4" + requizzle "^0.2.3" + strip-json-comments "^3.1.0" + taffydb "2.6.2" + underscore "~1.13.2" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -11450,6 +11508,13 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" + integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== + dependencies: + graceful-fs "^4.1.9" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -11610,6 +11675,13 @@ lines-and-columns@^2.0.2: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b" integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + lint-staged@^13.0.3: version "13.0.3" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.0.3.tgz#d7cdf03a3830b327a2b63c6aec953d71d9dc48c6" @@ -12205,11 +12277,32 @@ markdown-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3" integrity sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q== +markdown-it-anchor@^8.4.1: + version "8.6.5" + resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz#30c4bc5bbff327f15ce3c429010ec7ba75e7b5f8" + integrity sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ== + +markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + markdown-table@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c" integrity sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA== +marked@^4.0.10: + version "4.1.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.1.0.tgz#3fc6e7485f21c1ca5d6ec4a39de820e146954796" + integrity sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA== + matcher@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-5.0.0.tgz#cd82f1c7ae7ee472a9eeaf8ec7cac45e0fe0da62" @@ -12560,7 +12653,7 @@ mdast-util-toc@^6.1.0: unist-util-is "^5.0.0" unist-util-visit "^3.0.0" -mdurl@^1.0.0: +mdurl@^1.0.0, mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== @@ -16981,6 +17074,13 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +requizzle@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.3.tgz#4675c90aacafb2c036bd39ba2daa4a1cb777fded" + integrity sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ== + dependencies: + lodash "^4.17.14" + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -18306,6 +18406,11 @@ tabtab@^3.0.2: mkdirp "^0.5.1" untildify "^3.0.3" +taffydb@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" + integrity sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA== + tailwindcss-open-variant@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/tailwindcss-open-variant/-/tailwindcss-open-variant-1.0.0.tgz#e4555c0a0ec2a82801e563ed36b1b23e4dd04a3b" @@ -18901,6 +19006,11 @@ typical@^4.0.0: resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + uglify-js@^3.1.4: version "3.16.2" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.2.tgz#0481e1dbeed343ad1c2ddf3c6d42e89b7a6d4def" @@ -18936,7 +19046,7 @@ undefsafe@^2.0.2: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -underscore@^1.5.0: +underscore@^1.5.0, underscore@~1.13.2: version "1.13.4" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee" integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ== @@ -20000,6 +20110,11 @@ xml-js@^1.6.11: dependencies: sax "^1.2.4" +xmlcreate@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" + integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== + xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"