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"