2022-08-28 02:14:39 +02:00
|
|
|
import { Attributes } from './attributes.mjs'
|
|
|
|
import { Point } from './point.mjs'
|
2020-11-04 20:06:19 +01:00
|
|
|
import { Bezier } from 'bezier-js'
|
2018-08-22 15:55:15 +02:00
|
|
|
import {
|
2018-08-23 15:57:23 +02:00
|
|
|
linesIntersect,
|
|
|
|
lineIntersectsCurve,
|
|
|
|
curvesIntersect,
|
2018-08-22 15:55:15 +02:00
|
|
|
pointOnLine,
|
2018-08-31 09:44:12 +02:00
|
|
|
pointOnCurve,
|
2021-04-11 16:28:38 +02:00
|
|
|
curveEdge,
|
2021-04-24 10:16:31 +02:00
|
|
|
round,
|
2022-09-18 15:11:10 +02:00
|
|
|
__addNonEnumProp,
|
2022-10-07 21:41:45 +02:00
|
|
|
__asNumber,
|
2022-08-26 18:51:02 +02:00
|
|
|
} from './utils.mjs'
|
2018-07-14 16:04:39 +00:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
|
|
|
// CONSTRUCTOR //
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor for a Path
|
|
|
|
*
|
|
|
|
* @constructor
|
|
|
|
* @return {Path} this - The Path instance
|
|
|
|
*/
|
|
|
|
export function Path() {
|
2022-09-10 18:23:19 +02:00
|
|
|
// Enumerable properties
|
2022-09-18 17:01:19 +02:00
|
|
|
this.hidden = false
|
2022-09-10 18:23:19 +02:00
|
|
|
this.ops = []
|
|
|
|
this.attributes = new Attributes()
|
2019-08-03 15:03:33 +02:00
|
|
|
this.topLeft = false
|
|
|
|
this.bottomRight = false
|
2018-07-14 16:04:39 +00:00
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return this
|
|
|
|
}
|
2018-07-14 16:04:39 +00:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
|
|
|
// PUBLIC METHODS //
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-07-18 16:48:29 +02:00
|
|
|
Path.prototype._curve = function (cp2, to) {
|
2022-01-28 18:18:37 +01:00
|
|
|
if (to instanceof Point !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path._curve(cp2, to)` but `to` is not a `Point` object')
|
2022-01-28 18:18:37 +01:00
|
|
|
if (cp2 instanceof Point !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path._curve(cp2, to)` but `cp2` is not a `Point` object')
|
2019-08-03 15:03:33 +02:00
|
|
|
let cp1 = this.ops.slice(-1).pop().to
|
|
|
|
this.ops.push({ type: 'curve', cp1, cp2, to })
|
2018-12-21 11:44:56 +01:00
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return this
|
|
|
|
}
|
2018-12-21 11:44:56 +01:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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)
|
2018-07-21 12:54:29 +02:00
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return this
|
|
|
|
}
|
2018-07-21 12:54:29 +02:00
|
|
|
|
2022-09-27 18:22:56 +02:00
|
|
|
/**
|
|
|
|
* A chainable way to add text to a Path
|
|
|
|
*
|
|
|
|
* @param {string} text - The text to add to the Path
|
|
|
|
* @param {string} className - The CSS classes to apply to the text
|
|
|
|
* @return {Path} this - The Path instance
|
|
|
|
*/
|
|
|
|
Path.prototype.addText = function (text = '', className = false) {
|
|
|
|
this.attributes.add('data-text', text)
|
|
|
|
if (className) this.attributes.add('data-text-class', className)
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Returns the SVG pathstring for this path
|
|
|
|
*
|
|
|
|
* @return {string} svg - The SVG pathsstring (the 'd' attribute of an SVG path)
|
|
|
|
*/
|
2020-07-18 16:48:29 +02:00
|
|
|
Path.prototype.asPathstring = function () {
|
2019-08-03 15:03:33 +02:00
|
|
|
let d = ''
|
2018-07-23 20:14:32 +02:00
|
|
|
for (let op of this.ops) {
|
|
|
|
switch (op.type) {
|
2019-08-03 15:03:33 +02:00
|
|
|
case 'move':
|
2021-04-11 16:28:38 +02:00
|
|
|
d += `M ${round(op.to.x)},${round(op.to.y)}`
|
2019-08-03 15:03:33 +02:00
|
|
|
break
|
|
|
|
case 'line':
|
2021-04-11 16:28:38 +02:00
|
|
|
d += ` L ${round(op.to.x)},${round(op.to.y)}`
|
2019-08-03 15:03:33 +02:00
|
|
|
break
|
|
|
|
case 'curve':
|
2021-04-11 16:28:38 +02:00
|
|
|
d += ` C ${round(op.cp1.x)},${round(op.cp1.y)} ${round(op.cp2.x)},${round(
|
|
|
|
op.cp2.y
|
|
|
|
)} ${round(op.to.x)},${round(op.to.y)}`
|
2019-08-03 15:03:33 +02:00
|
|
|
break
|
|
|
|
case 'close':
|
|
|
|
d += ' z'
|
|
|
|
break
|
2018-07-14 16:04:39 +00:00
|
|
|
}
|
2018-07-23 20:14:32 +02:00
|
|
|
}
|
2018-07-14 16:04:39 +00:00
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return d
|
|
|
|
}
|
2018-07-23 11:12:06 +00:00
|
|
|
|
2023-06-04 16:52:02 +02:00
|
|
|
// Quick helper to return a drawing op as renderProps
|
|
|
|
const opAsrenderProp = (op) => {
|
|
|
|
const props = { type: op.type }
|
|
|
|
for (const p of ['from', 'to', 'cp1', 'cp2']) {
|
|
|
|
if (op[p]) props[p] = op[p].asRenderProps()
|
|
|
|
}
|
|
|
|
|
|
|
|
return props
|
|
|
|
}
|
|
|
|
|
feat(core): Added Pattern.getLogs() and updated Pattern.getRenderProps()
The data returned by `Pattern.getRenderProps()` was not serializable as
we were returning `this` all over the place, thereby including marcors,
log methods, cyclic object references, and so on.
This commit changes that by implementing a `.asRenderProp()` method on
all of the various objects (stack, part, path, point, snippet,
attributes, svg) and only including data that can be serialized.
In addition, we no longer include the logs in the renderProps because
they are not related to rendering the pattern.
Instead, the new method `Pattern.getLogs()` gives you the logs.
2023-06-01 16:45:13 +02:00
|
|
|
/**
|
|
|
|
* Returns a path as an object suitable for inclusion in renderprops
|
|
|
|
*
|
|
|
|
* @return {object} path - A plain object representing the path
|
|
|
|
*/
|
|
|
|
Path.prototype.asRenderProps = function () {
|
|
|
|
return {
|
|
|
|
attributes: this.attributes.asRenderProps(),
|
|
|
|
hidden: this.hidden,
|
|
|
|
name: this.name,
|
2023-06-04 16:52:02 +02:00
|
|
|
ops: this.ops.map((op) => opAsrenderProp(op)),
|
feat(core): Added Pattern.getLogs() and updated Pattern.getRenderProps()
The data returned by `Pattern.getRenderProps()` was not serializable as
we were returning `this` all over the place, thereby including marcors,
log methods, cyclic object references, and so on.
This commit changes that by implementing a `.asRenderProp()` method on
all of the various objects (stack, part, path, point, snippet,
attributes, svg) and only including data that can be serialized.
In addition, we no longer include the logs in the renderProps because
they are not related to rendering the pattern.
Instead, the new method `Pattern.getLogs()` gives you the logs.
2023-06-01 16:45:13 +02:00
|
|
|
topLeft: this.topLeft,
|
|
|
|
bottomRight: this.bottomRight,
|
|
|
|
width: this.bottomRight.x - this.topLeft.x,
|
|
|
|
height: this.bottomRight.y - this.topLeft.y,
|
|
|
|
d: this.asPathstring(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn(
|
2022-09-18 15:11:10 +02:00
|
|
|
'Called `Path.attr(name, value, overwrite=false)` but `name` is undefined or false'
|
|
|
|
)
|
|
|
|
if (typeof value === 'undefined')
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.attr(name, value, overwrite=false)` but `value` is undefined')
|
2022-09-18 15:11:10 +02:00
|
|
|
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)
|
2018-08-01 14:55:54 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return this
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-01 18:18:29 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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 = []
|
2019-08-03 15:03:33 +02:00
|
|
|
let current
|
2018-08-01 18:18:29 +02:00
|
|
|
for (let i in this.ops) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let op = this.ops[i]
|
2022-09-18 15:11:10 +02:00
|
|
|
if (op.type === 'line') {
|
|
|
|
bbs.push(__lineBoundingBox({ from: current, to: op.to }))
|
2019-08-03 15:03:33 +02:00
|
|
|
} else if (op.type === 'curve') {
|
2022-09-18 15:11:10 +02:00
|
|
|
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 }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2018-08-01 18:18:29 +02:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
if (op.to) current = op.to
|
2018-08-01 18:18:29 +02:00
|
|
|
}
|
|
|
|
|
2024-04-08 08:25:09 +02:00
|
|
|
if (bbs.length === 0 && current) {
|
|
|
|
// Degenerate case: Line is a point
|
|
|
|
bbs.push(__lineBoundingBox({ from: current, to: current }))
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return __bbbbox(bbs)
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-03 14:20:28 +02:00
|
|
|
|
2022-12-03 17:25:19 +01:00
|
|
|
/**
|
|
|
|
* Returns this after cleaning out in-place path operations
|
|
|
|
*
|
|
|
|
* Cleaned means that any in-place ops will be removed
|
|
|
|
* An in-place op is when a drawing operation doesn't draw anything
|
|
|
|
* like a line from the point to the same point
|
|
|
|
*
|
|
|
|
* @return {Path} this - This, but cleaned
|
|
|
|
*/
|
|
|
|
Path.prototype.clean = function () {
|
|
|
|
const ops = []
|
2022-12-04 20:19:51 -08:00
|
|
|
let cur
|
2022-12-03 17:25:19 +01:00
|
|
|
for (const i in this.ops) {
|
|
|
|
const op = this.ops[i]
|
|
|
|
if (['move', 'close', 'noop'].includes(op.type)) ops.push(op)
|
|
|
|
else if (op.type === 'line') {
|
|
|
|
if (!op.to.sitsRoughlyOn(cur)) ops.push(op)
|
|
|
|
} else if (op.type === 'curve') {
|
|
|
|
if (!(op.cp1.sitsRoughlyOn(cur) && op.cp2.sitsRoughlyOn(cur) && op.to.sitsRoughlyOn(cur)))
|
|
|
|
ops.push(ops)
|
|
|
|
}
|
2023-05-19 10:34:51 +02:00
|
|
|
cur = op.to
|
2022-12-03 17:25:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (ops.length < this.ops.length) this.ops = ops
|
|
|
|
|
2023-04-10 11:27:07 +02:00
|
|
|
// A path with not drawing operations or only a move is not path at all
|
|
|
|
return ops.length === 0 || (ops.length === 1 && ops[0].type === 'move') ? false : this
|
2022-12-03 17:25:19 +01:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Returns a deep copy of this path
|
|
|
|
*
|
|
|
|
* @return {Path} clone - A clone of this Path instance
|
|
|
|
*/
|
2020-07-18 16:48:29 +02:00
|
|
|
Path.prototype.clone = function () {
|
2022-09-18 17:01:19 +02:00
|
|
|
let clone = new Path().__withLog(this.log).setHidden(this.hidden)
|
2019-08-03 15:03:33 +02:00
|
|
|
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 = []
|
2018-08-03 14:20:28 +02:00
|
|
|
for (let i in this.ops) {
|
2019-08-03 15:03:33 +02:00
|
|
|
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()
|
2020-01-12 19:04:29 +01:00
|
|
|
} else if (op.type === 'noop') {
|
|
|
|
clone.ops[i].id = op.id
|
2018-08-03 14:20:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return clone
|
|
|
|
}
|
2018-08-03 14:20:28 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Adds a close operation
|
|
|
|
*
|
|
|
|
* @return {Path} this - The Path instance
|
|
|
|
*/
|
|
|
|
Path.prototype.close = function () {
|
|
|
|
this.ops.push({ type: 'close' })
|
2018-08-05 18:19:48 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return this
|
2018-08-05 18:19:48 +02:00
|
|
|
}
|
|
|
|
|
2024-02-10 15:40:41 +01:00
|
|
|
/**
|
|
|
|
* Combines one or more Paths into a single Path instance
|
|
|
|
*
|
|
|
|
* @param {array} paths - The paths to combine
|
|
|
|
* @return {Path} combo - The combined Path instance
|
|
|
|
*/
|
|
|
|
Path.prototype.combine = function (...paths) {
|
|
|
|
return __combinePaths(this, ...paths)
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.curve(cp1, cp2, to)` but `to` is not a `Point` object')
|
2022-09-18 15:11:10 +02:00
|
|
|
if (cp1 instanceof Point !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.curve(cp1, cp2, to)` but `cp1` is not a `Point` object')
|
2022-09-18 15:11:10 +02:00
|
|
|
if (cp2 instanceof Point !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.curve(cp1, cp2, to)` but `cp2` is not a `Point` object')
|
2022-09-18 15:11:10 +02:00
|
|
|
this.ops.push({ type: 'curve', cp1, cp2, to })
|
2018-08-08 15:53:07 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return this
|
2018-08-08 15:53:07 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.curve_(cp1, to)` but `to` is not a `Point` object')
|
2022-09-18 15:11:10 +02:00
|
|
|
if (cp1 instanceof Point !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.curve_(cp1, to)` but `cp1` is not a `Point` object')
|
2022-09-18 15:11:10 +02:00
|
|
|
let cp2 = to.copy()
|
|
|
|
this.ops.push({ type: 'curve', cp1, cp2, to })
|
2018-08-08 15:53:07 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return this
|
2018-08-08 15:53:07 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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
|
2018-08-09 10:46:14 +02:00
|
|
|
for (let i in this.ops) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let op = this.ops[i]
|
2022-09-18 15:11:10 +02:00
|
|
|
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))
|
2019-08-03 15:03:33 +02:00
|
|
|
} else if (op.type === 'curve') {
|
2022-09-18 15:11:10 +02:00
|
|
|
paths.push(new Path().__withLog(this.log).move(current).curve(op.cp1, op.cp2, op.to))
|
2019-08-03 15:03:33 +02:00
|
|
|
} else if (op.type === 'close') {
|
2022-09-18 15:11:10 +02:00
|
|
|
paths.push(new Path().__withLog(this.log).move(current).line(start))
|
2018-08-09 10:46:14 +02:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
if (op.to) current = op.to
|
2018-08-09 10:46:14 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return paths
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-09 10:46:14 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-07-18 16:48:29 +02:00
|
|
|
Path.prototype.edge = function (side) {
|
2022-09-18 15:11:10 +02:00
|
|
|
this.__boundary()
|
2019-08-03 15:03:33 +02:00
|
|
|
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)
|
2018-08-20 17:10:28 +02:00
|
|
|
else {
|
2019-08-03 15:03:33 +02:00
|
|
|
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
|
2018-08-20 17:10:28 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
} 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
|
|
|
|
)
|
2018-08-21 16:30:51 +02:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-21 16:30:51 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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]
|
2018-08-21 16:30:51 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
if (op.type === 'close') return this.start()
|
|
|
|
else return op.to
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-21 16:30:51 +02:00
|
|
|
|
2022-09-18 17:01:19 +02:00
|
|
|
/**
|
|
|
|
* Hide the path
|
|
|
|
*
|
|
|
|
* @return {Path} path - The Path instance
|
|
|
|
*/
|
|
|
|
Path.prototype.hide = function () {
|
|
|
|
this.hidden = true
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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) {
|
2023-09-05 12:00:05 +02:00
|
|
|
if (!noopId) this.log.warn('Called `Path.insop(noopId, path)` but `noopId` is undefined or false')
|
2022-09-18 15:11:10 +02:00
|
|
|
if (path instanceof Path !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.insop(noopId, path) but `path` is not a `Path` object')
|
2022-09-18 15:11:10 +02:00
|
|
|
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))
|
2018-08-21 16:30:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return newPath
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-21 16:30:51 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-07-18 16:48:29 +02:00
|
|
|
Path.prototype.intersects = function (path) {
|
2018-08-21 16:30:51 +02:00
|
|
|
if (this === path)
|
2022-09-10 18:23:19 +02:00
|
|
|
this.log.error('You called Path.intersects(path)` but `path` and `this` are the same object')
|
2019-08-03 15:03:33 +02:00
|
|
|
let intersections = []
|
2018-08-21 16:30:51 +02:00
|
|
|
for (let pathA of this.divide()) {
|
|
|
|
for (let pathB of path.divide()) {
|
2019-08-03 15:03:33 +02:00
|
|
|
if (pathA.ops[1].type === 'line') {
|
|
|
|
if (pathB.ops[1].type === 'line') {
|
2022-09-18 15:11:10 +02:00
|
|
|
__addIntersectionsToArray(
|
2019-08-03 15:03:33 +02:00
|
|
|
linesIntersect(pathA.ops[0].to, pathA.ops[1].to, pathB.ops[0].to, pathB.ops[1].to),
|
2018-08-21 16:30:51 +02:00
|
|
|
intersections
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
|
|
|
} else if (pathB.ops[1].type === 'curve') {
|
2022-09-18 15:11:10 +02:00
|
|
|
__addIntersectionsToArray(
|
2018-08-23 15:57:23 +02:00
|
|
|
lineIntersectsCurve(
|
|
|
|
pathA.ops[0].to,
|
|
|
|
pathA.ops[1].to,
|
2018-08-21 16:30:51 +02:00
|
|
|
pathB.ops[0].to,
|
|
|
|
pathB.ops[1].cp1,
|
|
|
|
pathB.ops[1].cp2,
|
2018-08-23 15:57:23 +02:00
|
|
|
pathB.ops[1].to
|
2018-08-21 16:30:51 +02:00
|
|
|
),
|
|
|
|
intersections
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
2018-08-21 16:30:51 +02:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
} else if (pathA.ops[1].type === 'curve') {
|
|
|
|
if (pathB.ops[1].type === 'line') {
|
2022-09-18 15:11:10 +02:00
|
|
|
__addIntersectionsToArray(
|
2018-08-23 15:57:23 +02:00
|
|
|
lineIntersectsCurve(
|
|
|
|
pathB.ops[0].to,
|
|
|
|
pathB.ops[1].to,
|
2018-08-21 16:30:51 +02:00
|
|
|
pathA.ops[0].to,
|
|
|
|
pathA.ops[1].cp1,
|
|
|
|
pathA.ops[1].cp2,
|
2018-08-23 15:57:23 +02:00
|
|
|
pathA.ops[1].to
|
2018-08-21 16:30:51 +02:00
|
|
|
),
|
|
|
|
intersections
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
|
|
|
} else if (pathB.ops[1].type === 'curve') {
|
2022-09-18 15:11:10 +02:00
|
|
|
__addIntersectionsToArray(
|
2018-08-23 15:57:23 +02:00
|
|
|
curvesIntersect(
|
2018-08-21 16:30:51 +02:00
|
|
|
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
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
2018-08-21 16:30:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return intersections
|
|
|
|
}
|
2018-08-21 16:30:51 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*
|
2024-02-10 15:40:41 +01:00
|
|
|
* The legacy (prior to v3.2) form of this method too two parameters:
|
|
|
|
* - The Path to join this path with
|
|
|
|
* - A boolean expressing whether the joined path should be closed
|
|
|
|
* In retrospect, that was kind of a dumb idea, because if the path
|
|
|
|
* needs tobe closed, you can juse chain the .join() with a .close()
|
|
|
|
*
|
|
|
|
* So now, this method is variadic, and it will join as many paths as you want.
|
|
|
|
* However, we keep it backwards compatible, and raise a deprecation warning when used that way.
|
|
|
|
*
|
|
|
|
* @param {array} paths - The Paths to join this Path with
|
2022-09-18 15:11:10 +02:00
|
|
|
* @return {Path} joint - The joint Path instance
|
|
|
|
*/
|
2024-02-10 15:40:41 +01:00
|
|
|
Path.prototype.join = function (...paths) {
|
|
|
|
if (paths.length < 1) {
|
2022-09-18 15:11:10 +02:00
|
|
|
this.log.error('Called `Path.join(that)` but `that` is not a `Path` object')
|
2024-02-10 15:40:41 +01:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check for legacy signature
|
|
|
|
*/
|
|
|
|
if (paths.length === 2 && [true, false].includes(paths[1])) {
|
|
|
|
this.log.warn(
|
|
|
|
'`Path.join()` was called with the legacy signature passing a bool as second parameter. This is deprecated and will be removed in FreeSewing v4'
|
|
|
|
)
|
|
|
|
return paths[1] ? __joinPaths([this, paths[0]]).close() : __joinPaths([this, paths[0]])
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* New variadic approach
|
|
|
|
*/
|
|
|
|
let i = 0
|
|
|
|
for (const path of paths) {
|
|
|
|
if (path instanceof Path !== true)
|
|
|
|
this.log.error(
|
|
|
|
`Called \`Path.join(paths)\` but the path with index \`${i}\` is not a \`Path\` object`
|
|
|
|
)
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
|
|
|
|
return __joinPaths([this, ...paths])
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the length of this Path
|
|
|
|
*
|
2024-02-10 15:40:41 +01:00
|
|
|
* @param {bool} withMoves - Include length of move operations inside the path
|
2022-09-18 15:11:10 +02:00
|
|
|
* @return {float} length - The length of this path
|
|
|
|
*/
|
2024-02-10 15:40:41 +01:00
|
|
|
Path.prototype.length = function (withMoves = false) {
|
2022-09-18 15:11:10 +02:00
|
|
|
let current, start
|
|
|
|
let length = 0
|
|
|
|
for (let i in this.ops) {
|
|
|
|
let op = this.ops[i]
|
|
|
|
if (op.type === 'move') {
|
2024-02-10 15:40:41 +01:00
|
|
|
if (typeof start === 'undefined') start = op.to
|
|
|
|
else if (withMoves) length += current.dist(op.to)
|
2022-09-18 15:11:10 +02:00
|
|
|
} else if (op.type === 'line') {
|
|
|
|
length += current.dist(op.to)
|
|
|
|
} else if (op.type === 'curve') {
|
|
|
|
length += 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 }
|
|
|
|
).length()
|
|
|
|
} else if (op.type === 'close') {
|
|
|
|
length += current.dist(start)
|
|
|
|
}
|
|
|
|
if (op.to) current = op.to
|
|
|
|
}
|
|
|
|
|
|
|
|
return length
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.line(to)` but `to` is not a `Point` object')
|
2022-09-18 15:11:10 +02:00
|
|
|
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)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.move(to)` but `to` is not a `Point` object')
|
2022-09-18 15:11:10 +02:00
|
|
|
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) {
|
2022-10-07 21:41:45 +02:00
|
|
|
distance = __asNumber(distance, 'distance', 'Path.offset', this.log)
|
|
|
|
|
2024-04-08 08:25:09 +02:00
|
|
|
return __pathOffset(this, distance, this.log)
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a reversed version of this Path
|
|
|
|
*
|
|
|
|
* @return {object} reverse - A Path instance that is the reversed version of this Path
|
|
|
|
*/
|
2022-09-27 18:22:56 +02:00
|
|
|
Path.prototype.reverse = function (cloneAttributes = false) {
|
2022-09-18 15:11:10 +02:00
|
|
|
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()
|
2022-09-27 18:22:56 +02:00
|
|
|
if (cloneAttributes) rev.attributes = this.attributes.clone()
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return rev
|
|
|
|
}
|
|
|
|
|
2024-04-05 14:13:22 +02:00
|
|
|
/**
|
|
|
|
* Returns a rotated version of this Path
|
|
|
|
* @param {number} deg Angle to rotate, see {@link Point#rotate}
|
|
|
|
* @param {Point} rotationOrigin point to use as rotation origin, see {@link Point#rotate}
|
|
|
|
* @param {boolean} cloneAttributes If the rotated path should receive a copy of the path attributes
|
|
|
|
*
|
|
|
|
* @return {Path} A Path instance that is a rotated copy of this Path
|
|
|
|
*/
|
|
|
|
Path.prototype.rotate = function (deg, rotationOrigin, cloneAttributes = false) {
|
|
|
|
deg = __asNumber(deg, 'deg', 'Path.rotate', this.log)
|
|
|
|
if (!(rotationOrigin instanceof Point))
|
|
|
|
this.log.warn('Called `Path.rotate(deg,that)` but `rotationOrigin` is not a `Point` object')
|
|
|
|
|
|
|
|
const rotatedPath = new Path().__withLog(this.log)
|
|
|
|
|
|
|
|
for (const op of this.ops) {
|
|
|
|
if (op.type === 'move') {
|
|
|
|
const to = op.to.rotate(deg, rotationOrigin)
|
|
|
|
rotatedPath.move(to)
|
|
|
|
} else if (op.type === 'line') {
|
|
|
|
const to = op.to.rotate(deg, rotationOrigin)
|
|
|
|
rotatedPath.line(to)
|
|
|
|
} else if (op.type === 'curve') {
|
|
|
|
const cp1 = op.cp1.rotate(deg, rotationOrigin)
|
|
|
|
const cp2 = op.cp2.rotate(deg, rotationOrigin)
|
|
|
|
const to = op.to.rotate(deg, rotationOrigin)
|
|
|
|
rotatedPath.curve(cp1, cp2, to)
|
|
|
|
} else if (op.type === 'close') {
|
|
|
|
rotatedPath.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (cloneAttributes) rotatedPath.attributes = this.attributes.clone()
|
|
|
|
|
|
|
|
return rotatedPath
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
}
|
|
|
|
|
2022-09-18 17:01:19 +02:00
|
|
|
/**
|
|
|
|
* Set the hidden attribute
|
|
|
|
*
|
|
|
|
* @param {boolean} hidden - The value to set the hidden property to
|
|
|
|
* @return {object} this - The Path instance
|
|
|
|
*/
|
|
|
|
Path.prototype.setHidden = function (hidden = false) {
|
|
|
|
if (hidden) this.hidden = true
|
|
|
|
else this.hidden = false
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-27 18:22:56 +02:00
|
|
|
/**
|
|
|
|
* A chainable way to set text on a Path
|
|
|
|
*
|
|
|
|
* @param {string} text - The text to add to the Path
|
|
|
|
* @param {string} className - The CSS classes to apply to the text
|
|
|
|
* @return {Path} this - The Path instance
|
|
|
|
*/
|
|
|
|
Path.prototype.setText = function (text = '', className = false) {
|
|
|
|
this.attributes.set('data-text', text)
|
|
|
|
if (className) this.attributes.set('data-text-class', className)
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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) {
|
2022-10-07 21:41:45 +02:00
|
|
|
distance = __asNumber(distance, 'distance', 'Path.shiftAlong', this.log)
|
2022-09-18 15:11:10 +02:00
|
|
|
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)
|
2022-09-19 18:04:47 +02:00
|
|
|
return __shiftAlongBezier(distance - len, bezier, thisLen * stepsPerMm)
|
2022-09-18 15:11:10 +02:00
|
|
|
len += thisLen
|
2018-08-21 16:30:51 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
current = op.to
|
2018-08-21 16:30:51 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
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)
|
2018-08-21 16:30:51 +02:00
|
|
|
}
|
2018-08-20 17:10:28 +02:00
|
|
|
|
2022-09-19 09:28:45 +02:00
|
|
|
/**
|
|
|
|
* Adds a smooth curve operation 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.smurve = function (cp2, to) {
|
|
|
|
if (to instanceof Point !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.smurve(cp2, to)` but `to` is not a `Point` object')
|
2022-09-19 09:28:45 +02:00
|
|
|
if (cp2 instanceof Point !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.smurve(cp2, to)` but `cp2` is not a `Point` object')
|
2022-09-19 09:28:45 +02:00
|
|
|
// Retrieve cp1 from previous operation
|
|
|
|
const prevOp = this.ops.slice(-1).pop()
|
|
|
|
const cp1 = prevOp.cp2.rotate(180, prevOp.to)
|
|
|
|
this.ops.push({ type: 'curve', cp1, cp2, to })
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a smooth curve operation without cp to Point to
|
|
|
|
*
|
|
|
|
* @return {Path} this - The Path instance
|
|
|
|
*/
|
|
|
|
Path.prototype.smurve_ = function (to) {
|
|
|
|
if (to instanceof Point !== true)
|
2023-09-05 12:00:05 +02:00
|
|
|
this.log.warn('Called `Path.smurve_(to)` but `to` is not a `Point` object')
|
2022-09-19 09:28:45 +02:00
|
|
|
// Retrieve cp1 from previous operation
|
|
|
|
const prevOp = this.ops.slice(-1).pop()
|
|
|
|
const cp1 = prevOp.cp2.rotate(180, prevOp.to)
|
|
|
|
const cp2 = to
|
|
|
|
this.ops.push({ type: 'curve', cp1, cp2, to })
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-07-18 16:48:29 +02:00
|
|
|
Path.prototype.split = function (point) {
|
|
|
|
if (point instanceof Point !== true)
|
2022-09-10 18:23:19 +02:00
|
|
|
this.log.error('Called `Path.split(point)` but `point` is not a `Point` object')
|
2019-08-03 15:03:33 +02:00
|
|
|
let divided = this.divide()
|
2022-03-29 19:32:07 +02:00
|
|
|
let firstHalf = []
|
|
|
|
let secondHalf = []
|
2019-05-10 13:14:31 +02:00
|
|
|
for (let pi = 0; pi < divided.length; pi++) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let path = divided[pi]
|
2023-12-15 06:50:58 +00:00
|
|
|
if (path.ops[0].to.sitsRoughlyOn(point)) {
|
|
|
|
divided[pi].ops[0].to = point.copy()
|
|
|
|
if (pi > 0) {
|
|
|
|
divided[pi - 1].ops[1].to = point.copy()
|
|
|
|
}
|
|
|
|
firstHalf = divided.slice(0, pi)
|
|
|
|
secondHalf = divided.slice(pi)
|
|
|
|
break
|
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
if (path.ops[1].type === 'line') {
|
2023-12-15 06:50:58 +00:00
|
|
|
if (pointOnLine(path.ops[0].to, path.ops[1].to, point)) {
|
2019-08-03 15:03:33 +02:00
|
|
|
firstHalf = divided.slice(0, pi)
|
2023-12-16 22:09:22 +00:00
|
|
|
firstHalf.push(new Path().__withLog(this.log).move(path.ops[0].to).line(point))
|
2019-08-03 15:03:33 +02:00
|
|
|
pi++
|
|
|
|
secondHalf = divided.slice(pi)
|
2023-12-16 22:09:22 +00:00
|
|
|
secondHalf.unshift(new Path().__withLog(this.log).move(point).line(path.ops[1].to))
|
2023-12-15 06:50:58 +00:00
|
|
|
break
|
2018-08-22 15:55:15 +02:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
} else if (path.ops[1].type === 'curve') {
|
2023-12-15 06:50:58 +00:00
|
|
|
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 }
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
2023-12-15 06:50:58 +00:00
|
|
|
|
|
|
|
let split = curve.split(t)
|
|
|
|
firstHalf = divided.slice(0, pi)
|
|
|
|
|
2022-05-31 15:57:53 +02:00
|
|
|
firstHalf.push(
|
2022-09-18 15:11:10 +02:00
|
|
|
new Path()
|
|
|
|
.__withLog(this.log)
|
2023-12-15 06:50:58 +00:00
|
|
|
.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),
|
|
|
|
point.copy()
|
|
|
|
)
|
2022-05-31 15:57:53 +02:00
|
|
|
)
|
2023-12-15 06:50:58 +00:00
|
|
|
pi++
|
|
|
|
|
|
|
|
secondHalf = divided.slice(pi)
|
|
|
|
secondHalf.unshift(
|
|
|
|
new Path()
|
|
|
|
.__withLog(this.log)
|
|
|
|
.move(point.copy())
|
|
|
|
.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)
|
|
|
|
)
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
2023-12-15 06:50:58 +00:00
|
|
|
break
|
2022-03-29 19:32:07 +02:00
|
|
|
}
|
2018-08-22 15:55:15 +02:00
|
|
|
}
|
|
|
|
}
|
2023-12-15 06:50:58 +00:00
|
|
|
|
2024-02-10 15:40:41 +01:00
|
|
|
if (firstHalf.length > 0) firstHalf = __joinPaths(firstHalf)
|
|
|
|
if (secondHalf.length > 0) secondHalf = __joinPaths(secondHalf)
|
2018-08-22 15:55:15 +02:00
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return [firstHalf, secondHalf]
|
|
|
|
}
|
2018-08-22 15:55:15 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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 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) {
|
2023-09-05 12:00:05 +02:00
|
|
|
if (typeof x !== 'number') this.log.warn('Called `Path.translate(x, y)` but `x` is not a number')
|
|
|
|
if (typeof y !== 'number') this.log.warn('Called `Path.translate(x, y)` but `y` is not a number')
|
2022-09-18 15:11:10 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes self-intersections (overlap) from this Path instance
|
|
|
|
*
|
|
|
|
* @return {Path} this - This Path instance
|
|
|
|
*/
|
2020-07-18 16:48:29 +02:00
|
|
|
Path.prototype.trim = function () {
|
2019-08-03 15:03:33 +02:00
|
|
|
let chunks = this.divide()
|
2019-05-10 13:14:31 +02:00
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let firstCandidate = parseInt(i) + 2
|
|
|
|
let lastCandidate = parseInt(chunks.length) - 1
|
2018-09-04 14:26:45 +02:00
|
|
|
for (let j = firstCandidate; j < lastCandidate; j++) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let intersections = chunks[i].intersects(chunks[j])
|
2018-09-04 14:26:45 +02:00
|
|
|
if (intersections.length > 0) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let intersection = intersections.pop()
|
|
|
|
let trimmedStart = chunks.slice(0, i)
|
|
|
|
let trimmedEnd = chunks.slice(parseInt(j) + 1)
|
2022-09-18 15:11:10 +02:00
|
|
|
let glue = new Path().__withLog(this.log)
|
2019-08-03 15:03:33 +02:00
|
|
|
let first = true
|
2018-09-04 15:21:59 +02:00
|
|
|
for (let k of [i, j]) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let ops = chunks[k].ops
|
|
|
|
if (ops[1].type === 'line') {
|
|
|
|
glue.line(intersection)
|
|
|
|
} else if (ops[1].type === 'curve') {
|
2018-09-04 15:21:59 +02:00
|
|
|
// 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 }
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
|
|
|
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
|
2018-09-04 15:21:59 +02:00
|
|
|
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)
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
2018-09-04 15:21:59 +02:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
first = false
|
2018-09-04 14:26:45 +02:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
let joint
|
2024-02-10 15:40:41 +01:00
|
|
|
if (trimmedStart.length > 0) joint = __joinPaths(trimmedStart).join(glue)
|
2019-08-03 15:03:33 +02:00
|
|
|
else joint = glue
|
2024-02-10 15:40:41 +01:00
|
|
|
if (trimmedEnd.length > 0) joint = joint.join(__joinPaths(trimmedEnd))
|
2018-09-04 14:26:45 +02:00
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return joint.trim()
|
2018-09-04 14:26:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return this
|
|
|
|
}
|
2018-09-04 14:26:45 +02:00
|
|
|
|
2022-09-18 17:01:19 +02:00
|
|
|
/**
|
2022-09-18 23:01:10 +02:00
|
|
|
* Unhide the path
|
2022-09-18 17:01:19 +02:00
|
|
|
*
|
|
|
|
* @return {Path} path - The Path instance
|
|
|
|
*/
|
|
|
|
Path.prototype.unhide = function () {
|
|
|
|
this.hidden = false
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
|
|
|
// 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
|
|
|
|
let topLeft = new Point(Infinity, Infinity)
|
|
|
|
let bottomRight = new Point(-Infinity, -Infinity)
|
|
|
|
let edges = []
|
|
|
|
for (let i in this.ops) {
|
|
|
|
let op = this.ops[i]
|
|
|
|
if (op.type === 'move' || op.type === 'line') {
|
|
|
|
if (op.to.x < topLeft.x) {
|
|
|
|
topLeft.x = op.to.x
|
|
|
|
edges['leftOp'] = i
|
|
|
|
}
|
|
|
|
if (op.to.y < topLeft.y) {
|
|
|
|
topLeft.y = op.to.y
|
|
|
|
edges['topOp'] = i
|
|
|
|
}
|
|
|
|
if (op.to.x > bottomRight.x) {
|
|
|
|
bottomRight.x = op.to.x
|
|
|
|
edges['rightOp'] = i
|
|
|
|
}
|
|
|
|
if (op.to.y > bottomRight.y) {
|
|
|
|
bottomRight.y = op.to.y
|
|
|
|
edges['bottomOp'] = i
|
|
|
|
}
|
|
|
|
} else if (op.type === 'curve') {
|
|
|
|
let bb = 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 }
|
|
|
|
).bbox()
|
|
|
|
if (bb.x.min < topLeft.x) {
|
|
|
|
topLeft.x = bb.x.min
|
|
|
|
edges['leftOp'] = i
|
|
|
|
}
|
|
|
|
if (bb.y.min < topLeft.y) {
|
|
|
|
topLeft.y = bb.y.min
|
|
|
|
edges['topOp'] = i
|
|
|
|
}
|
|
|
|
if (bb.x.max > bottomRight.x) {
|
|
|
|
bottomRight.x = bb.x.max
|
|
|
|
edges['rightOp'] = i
|
|
|
|
}
|
|
|
|
if (bb.y.max > bottomRight.y) {
|
|
|
|
bottomRight.y = bb.y.max
|
|
|
|
edges['bottomOp'] = i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (op.to) current = op.to
|
2020-07-18 16:48:29 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
this.topLeft = topLeft
|
|
|
|
this.bottomRight = bottomRight
|
|
|
|
|
|
|
|
for (let side of ['top', 'left', 'bottom', 'right']) {
|
|
|
|
let s = side + 'Op'
|
|
|
|
this[s] = this.ops[edges[s]]
|
|
|
|
this[s].from = this[s].type === 'move' ? this[s].to : this.ops[edges[s] - 1].to
|
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
)
|
2018-09-06 15:32:43 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return intersections
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
// 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)
|
2023-09-05 12:00:05 +02:00
|
|
|
log.warn(`\`paths.${name}\` was set with a value that is not a \`Path\` object`)
|
2022-09-18 15:11:10 +02:00
|
|
|
try {
|
|
|
|
value.name = name
|
|
|
|
} catch (err) {
|
2023-09-05 12:00:05 +02:00
|
|
|
log.warn(`Could not set \`name\` property on \`paths.${name}\``)
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
return (paths[name] = value)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
// PRIVATE STATIC METHODS //
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
2018-09-06 15:32:43 +02:00
|
|
|
}
|
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
2018-09-06 15:32:43 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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),
|
|
|
|
new Point(bezier.points[2].x, bezier.points[2].y),
|
|
|
|
new Point(bezier.points[3].x, bezier.points[3].y)
|
|
|
|
)
|
2022-12-03 17:25:19 +01:00
|
|
|
.clean()
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) }
|
|
|
|
}
|
|
|
|
|
2024-02-10 15:40:41 +01:00
|
|
|
/**
|
|
|
|
* Combines path segments into a single path instance
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Array} paths - An Array of Path objects
|
|
|
|
* @return {object} path - A Path instance
|
|
|
|
*/
|
|
|
|
function __combinePaths(...paths) {
|
|
|
|
const joint = new Path().__withLog(paths[0].log)
|
|
|
|
for (const path of paths) joint.ops.push(...path.ops)
|
|
|
|
|
|
|
|
return joint
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
* @return {object} path - A Path instance
|
|
|
|
*/
|
2024-02-10 15:40:41 +01:00
|
|
|
function __joinPaths(paths) {
|
2022-09-18 15:11:10 +02:00
|
|
|
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) {
|
|
|
|
if (op.type === 'curve') {
|
|
|
|
joint.curve(op.cp1, op.cp2, op.to)
|
2022-12-23 15:34:18 -08:00
|
|
|
} else if (op.type === 'noop') {
|
|
|
|
joint.noop(op.id)
|
2022-09-18 15:11:10 +02:00
|
|
|
} else if (op.type !== 'close') {
|
|
|
|
// We're using sitsRoughlyOn here to avoid miniscule line segments
|
|
|
|
if (current && !op.to.sitsRoughlyOn(current)) joint.line(op.to)
|
|
|
|
} else {
|
|
|
|
let err = 'Cannot join a closed path with another'
|
|
|
|
joint.log.error(err)
|
|
|
|
throw new Error(err)
|
|
|
|
}
|
|
|
|
if (op.to) current = op.to
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return joint
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
|
|
|
if (from.y < to.y) return { topLeft: from, bottomRight: to }
|
|
|
|
else return { topLeft: to, bottomRight: from }
|
|
|
|
} else if (from.y === to.y) {
|
|
|
|
if (from.x < to.x) return { topLeft: from, bottomRight: to }
|
|
|
|
else return { topLeft: to, bottomRight: from }
|
|
|
|
} else if (from.x < to.x) {
|
|
|
|
if (from.y < to.y) return { topLeft: from, bottomRight: to }
|
|
|
|
else
|
|
|
|
return {
|
|
|
|
topLeft: new Point(from.x, to.y),
|
|
|
|
bottomRight: new Point(to.x, from.y),
|
|
|
|
}
|
|
|
|
} else if (from.x > to.x) {
|
|
|
|
if (from.y < to.y)
|
|
|
|
return {
|
|
|
|
topLeft: new Point(to.x, from.y),
|
|
|
|
bottomRight: new Point(from.x, to.y),
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return {
|
|
|
|
topLeft: new Point(to.x, to.y),
|
|
|
|
bottomRight: new Point(from.x, from.y),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 new Path().__withLog(log).move(from.shift(angle, distance)).line(to.shift(angle, distance))
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Offsets a path by distance
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Path} path - The Path to offset
|
|
|
|
* @param {float} distance - The distance to offset by
|
|
|
|
* @return {Path} offsetted - The offsetted Path instance
|
|
|
|
*/
|
2024-04-08 08:25:09 +02:00
|
|
|
function __pathOffset(path, distance, log) {
|
2022-09-18 15:11:10 +02:00
|
|
|
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.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().__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 }
|
|
|
|
)
|
2023-04-10 11:27:07 +02:00
|
|
|
for (let bezier of b.offset(distance)) {
|
|
|
|
const segment = __asPath(bezier, path.log)
|
|
|
|
if (segment) offset.push(segment)
|
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
} else if (op.type === 'close') closed = true
|
|
|
|
if (op.to) current = op.to
|
|
|
|
if (!start) start = current
|
|
|
|
}
|
|
|
|
|
2024-04-08 08:25:09 +02:00
|
|
|
let result
|
|
|
|
|
|
|
|
if (offset.length !== 0) {
|
|
|
|
result = __joinPaths(offset)
|
|
|
|
} else {
|
|
|
|
// degenerate case: Original path was likely short, so all the "if (segment)" checks returned false
|
|
|
|
// retry treating the path as a simple straight line from start to end
|
|
|
|
// note: do not call __joinPaths in this branch as this could result in "over-optimizing" this short path
|
|
|
|
let segment = __offsetLine(start, current, distance, path.log)
|
|
|
|
if (segment) {
|
|
|
|
result = segment
|
|
|
|
} else {
|
|
|
|
result = new Path().move(start).line(current)
|
|
|
|
log.warn(`Could not properly calculate offset path, the given path is likely too short.`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return closed ? result.close() : result
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2022-09-19 18:04:47 +02:00
|
|
|
* @param {int} steps - The numer of steps per mm to walk the Bezier with
|
2022-09-18 15:11:10 +02:00
|
|
|
* @return {Point} point - The point at distance along the cubic Bezier curve
|
|
|
|
*/
|
2022-09-19 18:04:47 +02:00
|
|
|
function __shiftAlongBezier(distance, bezier, steps) {
|
2022-09-18 15:11:10 +02:00
|
|
|
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
|
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|